From a3b680c5af01b975ab76eae43142badb137e55b4 Mon Sep 17 00:00:00 2001 From: RDW Date: Sun, 13 Oct 2024 10:01:18 +0200 Subject: [PATCH 1/4] TOC: Removed the PTR and prepatch interface versions Listing several versions doesn't make sense since only the live realms are actually supported. This was only changed because the previous TOC version check took the shortcut of comparing Rarity's interface version to that of BigWigs, which (unlike Rarity) supports PTR testing. As the TOC check now involves fetching the latest version directly from Blizzard's CDN, this hack is no longer useful and can be removed. --- Modules/Options/Rarity_Options.toc | 4 ++-- Rarity.toc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Options/Rarity_Options.toc b/Modules/Options/Rarity_Options.toc index 1b1c7fa7..567f7ac9 100644 --- a/Modules/Options/Rarity_Options.toc +++ b/Modules/Options/Rarity_Options.toc @@ -1,6 +1,6 @@ ## Author: Allara -## Interface: 110005, 110000, 110002 -## X-Min-Interface: 110005 +## Interface: 110002 +## X-Min-Interface: 110002 ## Notes: Rarity configuration. Use AddonLoader to load this on demand. ## Title: Rarity [|caaedc99fOptions|r] ## Dependencies: Rarity diff --git a/Rarity.toc b/Rarity.toc index 232863c1..28dbcde1 100644 --- a/Rarity.toc +++ b/Rarity.toc @@ -1,5 +1,5 @@ ## Author: Allara -## Interface: 110005, 110000, 110002 +## Interface: 110002 ## X-Min-Interface: 110005 ## Title: Rarity ## Version: 1.0 (@project-version@) From 74d81ff2e0ee062b57d96068d121b3b8451dded4 Mon Sep 17 00:00:00 2001 From: RDW Date: Sun, 13 Oct 2024 09:54:21 +0200 Subject: [PATCH 2/4] CI: Implemented a more reliable TOC version check Instead of blindly copying BigWigs' TOC version, it's possible to query Blizzard's CDN directly. While this adds some complexity due to the oddball CSV-like format that they use, it should be more reliable. More advanced interactions like synchronization can now also be scripted in Lua, instead of having to work around various peculiarities of the different shells and terminal environments. --- .github/check-interface-versions.sh | 21 +---- .github/workflows/check-toc-version.yaml | 7 +- Tests/Fixtures/cdn-response-example.txt | 9 ++ .../cdn-response-malformed-header.txt | 9 ++ Tests/TOC/BlizzardTOC.lua | 22 +++++ Tests/TOC/CDN.lua | 89 +++++++++++++++++++ Tests/TOC/check-cdn-version.lua | 61 +++++++++++++ Tests/test-toc.spec.lua | 80 +++++++++++++++++ Tests/unit-test.lua | 1 + 9 files changed, 280 insertions(+), 19 deletions(-) create mode 100644 Tests/Fixtures/cdn-response-example.txt create mode 100644 Tests/Fixtures/cdn-response-malformed-header.txt create mode 100644 Tests/TOC/BlizzardTOC.lua create mode 100644 Tests/TOC/CDN.lua create mode 100644 Tests/TOC/check-cdn-version.lua create mode 100644 Tests/test-toc.spec.lua 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/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) From 51b998c51310cb354c91366c243a441518814354 Mon Sep 17 00:00:00 2001 From: RDW Date: Sat, 26 Oct 2024 07:33:16 +0200 Subject: [PATCH 3/4] TOC: Updated the interface version for 11.0.5 --- Modules/Options/Rarity_Options.toc | 4 ++-- Rarity.toc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Options/Rarity_Options.toc b/Modules/Options/Rarity_Options.toc index 567f7ac9..1e7f762b 100644 --- a/Modules/Options/Rarity_Options.toc +++ b/Modules/Options/Rarity_Options.toc @@ -1,7 +1,7 @@ ## Author: Allara -## Interface: 110002 -## X-Min-Interface: 110002 ## Notes: Rarity configuration. Use AddonLoader to load this on demand. +## Interface: 110005 +## X-Min-Interface: 110005 ## Title: Rarity [|caaedc99fOptions|r] ## Dependencies: Rarity ## X-Part-Of: Rarity diff --git a/Rarity.toc b/Rarity.toc index 28dbcde1..6c576196 100644 --- a/Rarity.toc +++ b/Rarity.toc @@ -1,5 +1,5 @@ ## Author: Allara -## Interface: 110002 +## Interface: 110005 ## X-Min-Interface: 110005 ## Title: Rarity ## Version: 1.0 (@project-version@) From 505c1018ab27b7ab8249c6a93c9a82a4ff0bec54 Mon Sep 17 00:00:00 2001 From: RDW Date: Sat, 26 Oct 2024 07:34:09 +0200 Subject: [PATCH 4/4] TOC: Updated the description for Rarity_Options Since AddonLoader is no longer used, the note may be confusing/ --- Modules/Options/Rarity_Options.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Options/Rarity_Options.toc b/Modules/Options/Rarity_Options.toc index 1e7f762b..ca3b9b4c 100644 --- a/Modules/Options/Rarity_Options.toc +++ b/Modules/Options/Rarity_Options.toc @@ -1,7 +1,7 @@ ## Author: Allara -## Notes: Rarity configuration. Use AddonLoader to load this on demand. ## Interface: 110005 ## X-Min-Interface: 110005 +## Notes: Rarity configuration. Should be loaded automatically on demand. ## Title: Rarity [|caaedc99fOptions|r] ## Dependencies: Rarity ## X-Part-Of: Rarity