From 1ed169785c1ae8124eeb4b4fdb881a6c784aebe0 Mon Sep 17 00:00:00 2001 From: AdamZ Date: Mon, 7 Sep 2026 07:06:03 -0700 Subject: [PATCH] Connect existing build comparison tests to the PoB Codes corpus --- .github/workflows/buildtest.yml | 51 +++------------------------ .github/workflows/updatebuildlist.yml | 37 ------------------- CONTRIBUTING.md | 2 +- docker-compose.yml | 14 ++++++-- spec/BuildDiff.sh | 14 ++++++-- spec/DiffOutput.lua | 6 ++-- spec/FetchTestBuilds.lua | 21 +++++++++++ spec/GenerateBuilds.lua | 25 +++++++------ 8 files changed, 64 insertions(+), 106 deletions(-) delete mode 100644 .github/workflows/updatebuildlist.yml create mode 100644 spec/FetchTestBuilds.lua diff --git a/.github/workflows/buildtest.yml b/.github/workflows/buildtest.yml index 6ee6ad43231..fc2b31be474 100644 --- a/.github/workflows/buildtest.yml +++ b/.github/workflows/buildtest.yml @@ -18,41 +18,17 @@ jobs: id: get-dev-ref run: | git fetch --depth=1 origin dev - echo "devref=$(git rev-parse origin/dev)" >> $GITHUB_OUTPUT + echo "devref=$(git rev-parse origin/dev)" >> "$GITHUB_OUTPUT" - name: Download Dev branch cache id: download-dev-ref-cache uses: dawidd6/action-download-artifact@3ecf4024886f219d9290351234889bfb45d1b9da with: - name: cache-devref-${{ steps.get-dev-ref.outputs.devref }} - path: /tmp/cache/ - if_no_artifact_found: warn - search_artifacts: true - # Dev ref cache contains the build list and build xmls. Use that one to keep tests reproducible - - name: Update static builds list from cache - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'true' }} - run: cat /tmp/cache/builds.txt > spec/builds.txt - - name: Download latest build list - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' }} - id: download-build-list - uses: dawidd6/action-download-artifact@3ecf4024886f219d9290351234889bfb45d1b9da - with: - name: builds.txt - path: /tmp/latestbuildlist/ - workflow: updatebuildlist.yml - if_no_artifact_found: warn - search_artifacts: true - - name: Update static builds list - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' && steps.download-build-list.outputs.found_artifact == 'true' }} - run: cat /tmp/latestbuildlist/builds.txt > spec/builds.txt - - name: Download latest build xmls - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' }} - uses: dawidd6/action-download-artifact@3ecf4024886f219d9290351234889bfb45d1b9da - with: - name: build-xmls + name: corpus-cache-devref-${{ steps.get-dev-ref.outputs.devref }} path: /tmp/cache/ if_no_artifact_found: warn search_artifacts: true - name: Calculate build xmls and differences between them + shell: bash run: | mkdir /tmp/cache || true # Make sure /tmp/cache exists. Ignore exit code chmod -R 777 /tmp/cache && docker compose run -v '/tmp/cache/:/cache' -e 'CACHEDIR=/cache' busted-diff | tee /tmp/dockerlog @@ -66,28 +42,9 @@ jobs: with: name: build-diff-output path: /tmp/artefact - - name: Save used build list into cache - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' }} - run: cp spec/builds.txt /tmp/cache/ - - name: Move xmls found in builds.txt to a new directory - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' && steps.download-build-list.outputs.found_artifact == 'true' }} - run: | - mkdir new-build-xmls - while IFS= read -r line; do - FILENAME="/tmp/cache/${line//[^a-zA-Z0-9]/}.xml" - if [ -f "$FILENAME" ]; then - mv "$FILENAME" "./new-build-xmls/" - fi - done < "spec/builds.txt" - - name: Upload new build xmls - if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' && steps.download-build-list.outputs.found_artifact == 'true' }} - uses: actions/upload-artifact@v4 - with: - name: build-xmls - path: './new-build-xmls/*' - name: Upload dev ref cache if: ${{ steps.download-dev-ref-cache.outputs.found_artifact == 'false' }} uses: actions/upload-artifact@v4 with: - name: cache-devref-${{ steps.get-dev-ref.outputs.devref }} + name: corpus-cache-devref-${{ steps.get-dev-ref.outputs.devref }} path: /tmp/cache/ diff --git a/.github/workflows/updatebuildlist.yml b/.github/workflows/updatebuildlist.yml deleted file mode 100644 index ea841152b01..00000000000 --- a/.github/workflows/updatebuildlist.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Update build list for tests -on: - schedule: - - cron: '20 4 * * *' - workflow_dispatch: -jobs: - update-builds-list: - runs-on: ubuntu-22.04 - steps: - - name: Checkout HEAD - uses: actions/checkout@v4 - - name: Install moreutils - run: sudo apt-get install -y moreutils - - name: Download latest build list - uses: dawidd6/action-download-artifact@3ecf4024886f219d9290351234889bfb45d1b9da - with: - name: builds.txt - if_no_artifact_found: warn - path: /tmp/latestbuildlist/ - - name: Update list - run: > - cat spec/builds.txt /tmp/latestbuildlist/builds.txt - <({ curl "https://pobarchives.com/api/builds?q=latest" & curl "https://pobarchives.com/api/builds?q=trending"; } - | jq -r '.builds[].build_info.build_link') - | tail -n 500 - | sort -u - | sponge builds.txt - - name: Print new builds list - run: cat builds.txt - - name: Save new build list - uses: actions/upload-artifact@v4 - with: - name: builds.txt - path: builds.txt - overwrite: true - retention-days: 3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 394aadee37d..a9e72ea48f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -206,7 +206,7 @@ While both can be ran locally it's recommended to use the provided docker image To run the unit tests run `docker compose up busted-tests`. This option uses the [Busted](https://olivinelabs.com/busted/) framework to test values returned by specific functions. For more details see the implementation of the tests in `spec/System/`. -To run the build difference tests run `docker compose up busted-diff`. `busted-diff` busted diff first computes a list of builds found in `spec/builds.txt` and `spec/TestBuilds/` while checked out to the current working changes. Then it uses git to checkout the branch specified by `DEVREF` or `dev` by default and computes the same builds. Lastly it finds the differences in the outputs generated from both, does some post processing, and prints all of the information out. +To run the build difference tests run `docker compose up busted-diff`. `busted-diff` first downloads the build corpus from `https://api.pob.codes/test-builds/corpus` and computes it alongside `spec/TestBuilds/` using the current working changes. Then it uses git to checkout the branch specified by `DEVREF` or `dev` by default and computes the same builds. Lastly it finds the differences in the outputs generated from both, does some post processing, and prints all of the information out. The downloaded builds and list stay with the calculated `DEVREF` cache and are reused together until that cache changes. `busted-diff` supports the following options through environment variables: - `DEVREF`: Defines the branch to use as the reference against which to compare builds computed with new changes. diff --git a/docker-compose.yml b/docker-compose.yml index b1982fef1bf..9907f99383f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,16 +33,24 @@ services: - ./:/workdir:ro busted-diff: #build: . - image: ghcr.io/paliak/busted-tests:latest + image: ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest environment: #Where in the container the folders are stored WORKDIR: /workdir HOME: /tmp DEVREF: ${DEVREF:-origin/dev} HEADREF: $HEADREF + LUA_PATH: "/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;;" + LUA_CPATH: "/usr/lib/lua/5.1/?.so;;" container_name: busted-diff tty: true - user: nobody:nobody - command: /bin/sh -c "dos2unix < /workdir/spec/BuildDiff.sh | /bin/sh" + # Dependencies used by the existing build-diff scripts; the image provides LuaJIT. + command: + - /bin/sh + - -ec + - | + apk add --no-cache git parallel libxml2-utils lua5.1-posix lua5.1-curl zlib-dev + luarocks install lua-zlib 1.4-0 + exec su -p nobody -s /bin/sh -c 'dos2unix < /workdir/spec/BuildDiff.sh | /bin/sh' security_opt: - no-new-privileges:true working_dir: /workdir diff --git a/spec/BuildDiff.sh b/spec/BuildDiff.sh index 180960a11d1..05768e07eb4 100644 --- a/spec/BuildDiff.sh +++ b/spec/BuildDiff.sh @@ -1,4 +1,5 @@ #!/bin/sh +set -eo pipefail umask 0 # If external cache dir has not been defined keep it inside the container @@ -17,14 +18,21 @@ git config --global --add advice.detachedHead false if [[ ! -z "$HEADREF" ]] then git diff --no-color "$HEADREF" -- /tmp/workdir/.busted /tmp/workdir/src/HeadlessWrapper.lua /tmp/workdir/spec/ > /tmp/HeadPatch && - git reset --hard "$HEADREF" && git clean -fd && git apply --allow-empty /tmp/HeadPatch + git reset --hard "$HEADREF" && git clean -fd && git apply --allow-empty --index /tmp/HeadPatch fi headsha=$(git rev-parse HEAD) devsha=$(git rev-parse "$DEVREF") +# Keep the input corpus with the calculated base, as in the existing workflow. +if [[ ! -f "$CACHEDIR/$devsha" ]]; then + curl --fail --show-error --silent https://api.pob.codes/test-builds/corpus -o "$CACHEDIR/corpus.json" + luajit spec/FetchTestBuilds.lua "$CACHEDIR" +fi +cp "$CACHEDIR/builds.txt" spec/builds.txt + rm -rf /tmp/headsha && mkdir /tmp/headsha -rm /tmp/workdir/src/Settings.xml +rm -f /tmp/workdir/src/Settings.xml cat /tmp/workdir/spec/builds.txt | dos2unix | parallel --will-cite --ungroup --pipe -N50 'LINKSBATCH="$(mktemp){#}"; cat > $LINKSBATCH; BUILDLINKS="$LINKSBATCH" BUILDCACHEPREFIX="/tmp/headsha" busted --lua=luajit -r generate' && \ BUILDCACHEPREFIX='/tmp/headsha' busted --lua=luajit -r generate && date > "/tmp/headsha/$headsha" && echo "[+] Build cache computed for $headsha (headsha)" || exit $? @@ -34,7 +42,7 @@ then # Keep new changes to tests related files git diff --no-color "$DEVREF" -- /tmp/workdir/.busted /tmp/workdir/src/HeadlessWrapper.lua /tmp/workdir/spec/ > /tmp/DevPatch && \ - git reset --hard "$DEVREF" && git clean -fd && git apply --allow-empty /tmp/DevPatch && \ + git reset --hard "$DEVREF" && git clean -fd && git apply --allow-empty --index /tmp/DevPatch && \ cat /tmp/workdir/spec/builds.txt | dos2unix | parallel --will-cite --ungroup --pipe -N50 'LINKSBATCH="$(mktemp){#}"; cat > $LINKSBATCH; BUILDLINKS="$LINKSBATCH" BUILDCACHEPREFIX="$CACHEDIR" busted --lua=luajit -r generate' && \ BUILDCACHEPREFIX="$CACHEDIR" busted --lua=luajit -r generate && date > "$CACHEDIR/$devsha" && echo "[+] Build cache computed for $devsha (devsha)" || exit $? fi diff --git a/spec/DiffOutput.lua b/spec/DiffOutput.lua index 5f3228f21c0..949c36ba44e 100644 --- a/spec/DiffOutput.lua +++ b/spec/DiffOutput.lua @@ -8,11 +8,13 @@ local function buildOutputMap(filecontent) local playerOutput = {} local minionOutput = {} for line in splitLines(filecontent) do - local key, val = line:match('PlayerStat stat="(.-)" value="(.-)"') + local stat = line:match(']+)') + local key, val = stat and stat:match('stat="(.-)"'), stat and stat:match('value="(.-)"') if key then playerOutput[key] = val else - local key,val = line:match('MinionStat stat="(.-)" value="(.-)"') + local stat = line:match(']+)') + local key, val = stat and stat:match('stat="(.-)"'), stat and stat:match('value="(.-)"') if key then minionOutput[key] = val end diff --git a/spec/FetchTestBuilds.lua b/spec/FetchTestBuilds.lua new file mode 100644 index 00000000000..82123f141ba --- /dev/null +++ b/spec/FetchTestBuilds.lua @@ -0,0 +1,21 @@ +-- Store the API corpus alongside the calculated base cache, outside Git. +package.path = "runtime/lua/?.lua;" .. package.path +local json = require("dkjson") +local base64 = require("base64") +local zlib = require("zlib") +local cache = assert(arg[1]) +local input = assert(io.open(cache .. "/corpus.json", "r")) +local corpus = assert(json.decode(input:read("*a"))) +input:close() +assert(corpus.schemaVersion == 2 and #corpus.builds > 0 and #corpus.builds == corpus.count, "Invalid build corpus") +local list = assert(io.open(cache .. "/builds.txt", "w")) +for _, entry in ipairs(corpus.builds) do + assert(#entry.sha256 == 64 and entry.sha256:match("^%x+$"), "Invalid build filename") + local xml = zlib.inflate()(base64.decode(entry.code:gsub("-", "+"):gsub("_", "/"))) + local output = assert(io.open(cache .. "/" .. entry.sha256 .. ".xml", "w")) + output:write(xml) + output:close() + list:write(entry.sha256, "\n") +end +list:close() +print("[+] Downloaded " .. corpus.count .. " builds from corpus " .. corpus.snapshotId) diff --git a/spec/GenerateBuilds.lua b/spec/GenerateBuilds.lua index 9ed6c9de924..dc3be4fe325 100644 --- a/spec/GenerateBuilds.lua +++ b/spec/GenerateBuilds.lua @@ -8,16 +8,15 @@ local function fetchBuilds(path) fileHnd:close() for line in splitLines(fileText) do if line ~= "" then - for j = 1, #buildSites.websiteList do - if line:match(buildSites.websiteList[j].matchURL) then - local filename = line:gsub('%W', '') - - -- Load from cache if downloaded already - local fileHnd = io.open( (os.getenv("CACHEDIR") or "/tmp") .. "/" .. filename .. ".xml", "r") - if fileHnd then - coroutine.yield({ xml = fileHnd:read("*a"), filename = filename, link = line }) - fileHnd:close() - else + local filename = line:gsub('%W', '') + -- Check cached XML before matching a download provider. + local fileHnd = io.open((os.getenv("CACHEDIR") or "/tmp") .. "/" .. filename .. ".xml", "r") + if fileHnd then + coroutine.yield({ xml = fileHnd:read("*a"), filename = filename, link = line }) + fileHnd:close() + else + for j = 1, #buildSites.websiteList do + if line:match(buildSites.websiteList[j].matchURL) then -- Throttle build downloads to 15 per 10 seconds local timeSinceLastDL = GetTime() - lastDLtime if timeSinceLastDL < 666 then @@ -35,10 +34,10 @@ local function fetchBuilds(path) print("Failed to download build: " .. line) end end) + break + elseif j == #buildSites.websiteList then + print("Failed to match provider for: " .. line) end - break - elseif j == #buildSites.websiteList then - print("Failed to match provider for: " .. line) end end end