Skip to content

Commit

Permalink
Merge pull request #785 from WowRarity/cdn-version-checker
Browse files Browse the repository at this point in the history
Implement a more reliable TOC version check (using Blizzard's CDN)
  • Loading branch information
rdw-software authored Oct 26, 2024
2 parents b686361 + 505c101 commit 05caf4c
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 22 deletions.
21 changes: 3 additions & 18 deletions .github/check-interface-versions.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,4 @@
local_version=$(cat Rarity.toc | grep -oP '## Interface: \K(\d{6},? ?)+')
options_version=$(cat Modules/Options/Rarity_Options.toc | grep -oP '## Interface: \K(\d{6},? ?)+')
set -eu

# Since there's no "official" way to get the latest version, just use a popular/frequently updated addon ...
remote_version=$(curl https://raw.githubusercontent.com/BigWigsMods/BigWigs/master/BigWigs.toc --silent | grep -oP '## Interface: \K(\d{6},? ?)+')

if [ "$local_version" != "$remote_version" ]; then
echo "✗ Local interface version ($local_version) does NOT match remote version ($remote_version)"
exit 1
else
echo "✓ Local interface version ($local_version) matches remote version ($remote_version)"
fi

if [ "$local_version" != "$options_version" ]; then
echo "✗ Core interface version ($local_version) does NOT match options version ($options_version)"
exit 1
else
echo "✓ Core interface version ($local_version) matches options version ($options_version)"
fi
curl --silent --show-error https://us.version.battle.net/v2/products/wow/versions > cdn-response.txt
evo Tests/TOC/check-cdn-version.lua cdn-response.txt
7 changes: 6 additions & 1 deletion .github/workflows/check-toc-version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ on:
jobs:
toc:
name: Check interface versions
runs-on: ubuntu-latest
runs-on: macos-latest

steps:
- name: Check out Git repository
uses: actions/checkout@v4

- name: Install Lua runtime # May be overkill, but removes the need for complex shell scripting
uses: evo-lua/evo-setup-action@main
with:
version: 'v0.0.20'

- name: Check for outdated interface versions
run: .github/check-interface-versions.sh
4 changes: 2 additions & 2 deletions Modules/Options/Rarity_Options.toc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Author: Allara
## Interface: 110005, 110000, 110002
## Interface: 110005
## X-Min-Interface: 110005
## Notes: Rarity configuration. Use AddonLoader to load this on demand.
## Notes: Rarity configuration. Should be loaded automatically on demand.
## Title: Rarity [|caaedc99fOptions|r]
## Dependencies: Rarity
## X-Part-Of: Rarity
Expand Down
2 changes: 1 addition & 1 deletion Rarity.toc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Author: Allara
## Interface: 110005, 110000, 110002
## Interface: 110005
## X-Min-Interface: 110005
## Title: Rarity
## Version: 1.0 (@project-version@)
Expand Down
9 changes: 9 additions & 0 deletions Tests/Fixtures/cdn-response-example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Region!STRING:0|BuildConfig!HEX:16|CDNConfig!HEX:16|KeyRing!HEX:16|BuildId!DEC:4|VersionsName!String:0|ProductConfig!HEX:16
## seqn = 2515340
us|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
eu|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
cn|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
kr|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
tw|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
sg|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
xx|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
9 changes: 9 additions & 0 deletions Tests/Fixtures/cdn-response-malformed-header.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Region|BuildConfig!HEX:16|CDNConfig!HEX:16|KeyRing!HEX:16|BuildId!DEC:4|VersionsName!String:0|ProductConfig!HEX:16
## seqn = 2515340
us|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
eu|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
cn|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
kr|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
tw|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
sg|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
xx|afb222415432704dab1c5849cfd3e39f|bba400d95ca3cbf8a0912ec7c9d8899d|3ca57fe7319a297346440e4d2a03a0cd|57212|11.0.5.57212|53020d32e1a25648c8e1eafd5771935f
22 changes: 22 additions & 0 deletions Tests/TOC/BlizzardTOC.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
local BlizzardTOC = {}

function BlizzardTOC:DecodeFileContents(fileContents)
local toc = {}

local lines = string.explode(fileContents, "\n")
for _, line in ipairs(lines) do
line = line:gsub("\r", "") -- Avoids crossplatform headaches (autoformat doesn't modify TOC files)
toc["Title"] = toc["Title"] or line:match("^## Title: (.+)")
toc["Author"] = toc["Author"] or line:match("^## Author: (.+)")
toc["Interface"] = toc["Interface"] or tonumber(line:match("^## Interface: (%d+)"))
toc["X-Min-Interface"] = toc["X-Min-Interface"] or tonumber(line:match("^## X%-Min%-Interface: (%d+)"))
toc["X-Curse-Project-ID"] = toc["X-Curse-Project-ID"]
or tonumber(line:match("^## X%-Curse%-Project%-ID: (%d+)"))
toc["Dependencies"] = toc["Dependencies"] or line:match("^## Dependencies: (.+)")
toc["X-Part-Of"] = toc["X-Part-Of"] or line:match("^## X%-%Part%-Of: (.+)")
end

return toc
end

return BlizzardTOC
89 changes: 89 additions & 0 deletions Tests/TOC/CDN.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
local assert = assert
local ipairs = ipairs
local tonumber = tonumber

local string_explode = string.explode
local table_insert = table.insert

local CDN = {
SEQUENCE_NUMBER_PATTERN = "## seqn = ",
TRIM_WHITESPACE_PATTERN = "^%s*(.-)%s*$",
errorStrings = {
INVALID_VERSION_FORMAT = "Invalid CDN version string format (should be be MAJOR.MINOR.PATCH)",
MALFORMED_RESPONSE_HEADER = "Malformed CDN response header (should be !-separated key-value pairs)",
},
}

function CDN:VersionNameToInterfaceVersion(versionName)
local tokens = string_explode(versionName, ".")

if #tokens < 3 then
return nil, CDN.errorStrings.INVALID_VERSION_FORMAT
end

local major = tokens[1]
local minor = tokens[2]
local patch = tokens[3]

return tonumber(major) * 10000 + tonumber(minor) * 100 + tonumber(patch)
end

local function parseNextLine(response, line)
line = line:match(CDN.TRIM_WHITESPACE_PATTERN)

assert(line ~= nil)
assert(line ~= "")

-- Parse seqn (second line)
if line:find(CDN.SEQUENCE_NUMBER_PATTERN) then
local sequenceNumber = line:gsub(CDN.SEQUENCE_NUMBER_PATTERN, "")
response.sequenceNumber = tonumber(sequenceNumber)
return
end

local tokens = string_explode(line, "|")

-- Parse header (first line)
if #response.csvFieldNames == 0 then
for _, csvFieldName in ipairs(tokens) do
local tokens = string_explode(csvFieldName, "!")
if #tokens ~= 2 then
error(CDN.errorStrings.MALFORMED_RESPONSE_HEADER, 0)
end

table_insert(response.csvFieldNames, tokens[1])
end

return
end

-- Parse the CSV data (subsequent lines)
local regionKey = tokens[1]
local csvEntry = {}

for index, csvValue in ipairs(tokens) do
local fieldName = response.csvFieldNames[index]
assert(fieldName ~= nil)
csvEntry[fieldName] = csvValue
end

assert(response.productInfoByRegion[regionKey] == nil)
response.productInfoByRegion[regionKey] = csvEntry
end

function CDN:ParseResponseText(data)
local response = {
csvFieldNames = {},
productInfoByRegion = {},
}

local lines = string_explode(data, "\n")
for _, line in ipairs(lines) do
-- Might be faster to use goto continue here, but it breaks the autoformatter (revisit later?)
parseNextLine(response, line)
end

return response
end

return CDN
61 changes: 61 additions & 0 deletions Tests/TOC/check-cdn-version.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
local BlizzardTOC = require("Tests.TOC.BlizzardTOC")
local CDN = require("Tests.TOC.CDN")

local transform = require("transform")
local bold = transform.bold

local cdnResponseText = C_FileSystem.ReadFile(arg[1])
C_FileSystem.Delete(arg[1])
local coreAddonVersion = arg[2]
local optionsAddonVersion = arg[3]

printf("Parsing CDN response:\n%s", transform.cyan(cdnResponseText))
local response = CDN:ParseResponseText(cdnResponseText)
printf(transform.yellow("Sequence number: %d"), response.sequenceNumber)
printf(transform.yellow("Region keys: %s"), dump(table.keys(response.productInfoByRegion), { silent = true }))

print()

for regionKey, productInfo in pairs(response.productInfoByRegion) do
if regionKey == "us" then
for key, value in pairs(productInfo) do
printf(bold("%s") .. ": %s", key, value)
end
end
end

print()

local tocFiles = {
Core = "Rarity.toc",
Options = "Modules/Options/Rarity_Options.toc",
}

for moduleName, tocFilePath in pairs(tocFiles) do
local tocFileContents = C_FileSystem.ReadFile(tocFilePath)
printf("Processing TOC file: %s -> %s", bold(moduleName), bold(tocFilePath))
local toc = BlizzardTOC:DecodeFileContents(tocFileContents)

local tocInterfaceVersion = toc["Interface"]
printf(bold("Detected interface version: %d"), tocInterfaceVersion)

-- Assumes the US CDN is authoritative (should be the earliest to update?)
local usVersionName = response.productInfoByRegion.us.VersionsName
local latestInterfaceVersion = CDN:VersionNameToInterfaceVersion(usVersionName)

if tocInterfaceVersion ~= latestInterfaceVersion then
local errorMessage = format(
"✗ Local TOC interface version %d does NOT match Blizzard CDN version %d",
tocInterfaceVersion,
latestInterfaceVersion
)
error(transform.red(errorMessage))
else
printf(
transform.green("✓ Local TOC interface version %d matches Blizzard CDN version %d"),
tocInterfaceVersion,
latestInterfaceVersion
)
end
print()
end
80 changes: 80 additions & 0 deletions Tests/test-toc.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
local BlizzardTOC = require("Tests.TOC.BlizzardTOC")
local CDN = require("Tests.TOC.CDN")

local VALID_RESPONSE_FILE = path.join("Tests", "Fixtures", "cdn-response-example.txt")
local VALID_RESPONSE_TEXT = C_FileSystem.ReadFile(VALID_RESPONSE_FILE)
local INVALID_RESPONSE_FILE = path.join("Tests", "Fixtures", "cdn-response-malformed-header.txt")
local INVALID_RESPONSE_TEXT = C_FileSystem.ReadFile(INVALID_RESPONSE_FILE)

local RARITY_CORE_TOC = C_FileSystem.ReadFile("Rarity.toc")
local RARITY_OPTIONS_TOC = C_FileSystem.ReadFile(path.join("Modules", "Options", "Rarity_Options.toc"))

local EXAMPLE_PRODUCT_INFO = {
BuildConfig = "afb222415432704dab1c5849cfd3e39f",
BuildId = "57212",
CDNConfig = "bba400d95ca3cbf8a0912ec7c9d8899d",
KeyRing = "3ca57fe7319a297346440e4d2a03a0cd",
ProductConfig = "53020d32e1a25648c8e1eafd5771935f",
Region = "us",
VersionsName = "11.0.5.57212",
}

describe("TOC", function()
describe("BlizzardTOC", function()
describe("DecodeFileContents", function()
it("should be able to load valid TOC files", function()
local RarityCoreTOC = BlizzardTOC:DecodeFileContents(RARITY_CORE_TOC)
local RarityOptionsTOC = BlizzardTOC:DecodeFileContents(RARITY_OPTIONS_TOC)

-- For now, only parse the header (other fields can be added as needed)
assertEquals(RarityCoreTOC["Title"], "Rarity")
assertEquals(RarityCoreTOC["Author"], "Allara")
assertTrue(RarityCoreTOC["Interface"] > 0)
assertEquals(type(RarityCoreTOC["X-Min-Interface"]), "number")
assertEquals(RarityCoreTOC["X-Curse-Project-ID"], 30801)
assertEquals(RarityCoreTOC["Interface"], RarityCoreTOC["X-Min-Interface"])

assertEquals(RarityOptionsTOC["Title"], "Rarity [|caaedc99fOptions|r]")
assertEquals(RarityOptionsTOC["Dependencies"], "Rarity")
assertEquals(RarityOptionsTOC["X-Part-Of"], "Rarity")

assertEquals(RarityCoreTOC["Author"], RarityOptionsTOC["Author"])
assertEquals(RarityCoreTOC["Interface"], RarityOptionsTOC["Interface"])
assertEquals(RarityCoreTOC["X-Min-Interface"], RarityOptionsTOC["X-Min-Interface"])
end)
end)
end)

describe("CDN", function()
describe("VersionNameToInterfaceVersion", function()
it("should return a number representing the TOC interface version", function()
assertEquals(CDN:VersionNameToInterfaceVersion("11.0.5.57212"), 110005)
end)

it("should fail if the provided version name has an invalid format", function()
assertFailure(function()
return CDN:VersionNameToInterfaceVersion("11.0")
end, CDN.errorStrings.INVALID_VERSION_FORMAT)
end)
end)

describe("ParseResponseText", function()
it("should throw if the header's key-value format is not as expected", function()
assertThrows(function()
CDN:ParseResponseText(INVALID_RESPONSE_TEXT)
end, CDN.errorStrings.MALFORMED_RESPONSE_HEADER)
end)

it("should return a table representing the CDN response body", function()
local expectedFieldNames =
{ "Region", "BuildConfig", "CDNConfig", "KeyRing", "BuildId", "VersionsName", "ProductConfig" }

local response = CDN:ParseResponseText(VALID_RESPONSE_TEXT)
assertEquals(response.sequenceNumber, 2515340)
assertEquals(#response.csvFieldNames, 7)
assertEquals(response.csvFieldNames, expectedFieldNames)
assertEquals(response.productInfoByRegion.us, EXAMPLE_PRODUCT_INFO) -- Don't care about the rest
end)
end)
end)
end)
1 change: 1 addition & 0 deletions Tests/unit-test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local specFiles = {
"Tests/test-database.spec.lua",
"Tests/test-holiday-events.spec.lua",
"Tests/test-serialization.spec.lua",
"Tests/test-toc.spec.lua",
}

local numFailedSections = C_Runtime.RunDetailedTests(specFiles)
Expand Down

0 comments on commit 05caf4c

Please sign in to comment.