diff --git a/.github/check-interface-versions.sh b/.github/check-interface-versions.sh index d5821f7b..a4dfb5c3 100755 --- a/.github/check-interface-versions.sh +++ b/.github/check-interface-versions.sh @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/.github/workflows/check-toc-version.yaml b/.github/workflows/check-toc-version.yaml index 60bf52c5..7658c25a 100644 --- a/.github/workflows/check-toc-version.yaml +++ b/.github/workflows/check-toc-version.yaml @@ -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 diff --git a/Modules/Options/Rarity_Options.toc b/Modules/Options/Rarity_Options.toc index 1b1c7fa7..ca3b9b4c 100644 --- a/Modules/Options/Rarity_Options.toc +++ b/Modules/Options/Rarity_Options.toc @@ -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 diff --git a/Rarity.toc b/Rarity.toc index 232863c1..6c576196 100644 --- a/Rarity.toc +++ b/Rarity.toc @@ -1,5 +1,5 @@ ## Author: Allara -## Interface: 110005, 110000, 110002 +## Interface: 110005 ## X-Min-Interface: 110005 ## Title: Rarity ## Version: 1.0 (@project-version@) diff --git a/Tests/Fixtures/cdn-response-example.txt b/Tests/Fixtures/cdn-response-example.txt new file mode 100644 index 00000000..3b8f201f --- /dev/null +++ b/Tests/Fixtures/cdn-response-example.txt @@ -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 diff --git a/Tests/Fixtures/cdn-response-malformed-header.txt b/Tests/Fixtures/cdn-response-malformed-header.txt new file mode 100644 index 00000000..a427304c --- /dev/null +++ b/Tests/Fixtures/cdn-response-malformed-header.txt @@ -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 diff --git a/Tests/TOC/BlizzardTOC.lua b/Tests/TOC/BlizzardTOC.lua new file mode 100644 index 00000000..9a836718 --- /dev/null +++ b/Tests/TOC/BlizzardTOC.lua @@ -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 diff --git a/Tests/TOC/CDN.lua b/Tests/TOC/CDN.lua new file mode 100644 index 00000000..94818481 --- /dev/null +++ b/Tests/TOC/CDN.lua @@ -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 diff --git a/Tests/TOC/check-cdn-version.lua b/Tests/TOC/check-cdn-version.lua new file mode 100644 index 00000000..067569d4 --- /dev/null +++ b/Tests/TOC/check-cdn-version.lua @@ -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 diff --git a/Tests/test-toc.spec.lua b/Tests/test-toc.spec.lua new file mode 100644 index 00000000..ec2af331 --- /dev/null +++ b/Tests/test-toc.spec.lua @@ -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) diff --git a/Tests/unit-test.lua b/Tests/unit-test.lua index c67f236f..4f4fc6ef 100644 --- a/Tests/unit-test.lua +++ b/Tests/unit-test.lua @@ -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)