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 ba990338634..b884cfbe53c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -305,7 +305,7 @@ While both can be ran locally it's recommended to use the provided docker image PoB uses the [Busted](https://lunarmodules.github.io/busted/) framework to run its tests. Tests are stored under `spec/System` and run automatically when a PR is modified. More tests can be added to this folder to test specific functionality, or new test builds can be added to ensure nothing changed that wasn't intended. -### Running tests +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. 1. Install [Docker](https://www.docker.com/get-started) 2. Run `docker-compose up` from the command line diff --git a/docker-compose.yml b/docker-compose.yml index b01cfb360ab..273407c83e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,3 +13,28 @@ services: working_dir: /workdir volumes: - ./:/workdir:ro + busted-diff: + #build: . + 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 + # 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 + volumes: + - ./:/workdir:ro 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