From acd09f42f519aea11b34df2ff79c29f12732a831 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 19:14:21 +0100 Subject: [PATCH 01/11] Add integration tests --- .github/workflows/integration-tests.yml | 65 ++++++++ .gitignore | 5 +- package.json | 7 +- test/README.md | 49 ++++++ test/integration.test.js | 210 ++++++++++++++++++++++++ 5 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 test/README.md create mode 100644 test/integration.test.js diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..06f43ec --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,65 @@ +name: Integration Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + - run: pnpm i --no-frozen-lockfile + - run: node build.js + # Upload build to artifacts + - name: Upload build to artifacts + uses: actions/upload-artifact@v4 + with: + name: subspace_storage-builds + path: ./dist + if-no-files-found: error + + test: + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + factorio-version: ['0.17.79', '1.0.0', '1.1.110', '2.0.39'] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + + # Install xz-utils for tar.xz extraction + - name: Install xz-utils + run: sudo apt-get update && sudo apt-get install -y xz-utils + + # Download the built artifacts + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: subspace_storage-builds + path: ./dist + + - name: Install dependencies + run: pnpm i --no-frozen-lockfile + + - name: Run integration test + env: + FACTORIO_VERSION: ${{ matrix.factorio-version }} + run: node test/integration.test.js diff --git a/.gitignore b/.gitignore index 0f6e932..d9ab34c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,7 @@ node_modules/ package-lock.json # Blender save versions -*.blend[1-9] \ No newline at end of file +*.blend[1-9] + +# Factorio test files +test/factorio* diff --git a/package.json b/package.json index 0f7b4ed..01c240d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "repository": "https://github.com/clusterio/subspace_storage", "license": "MIT", "scripts": { - "build": "node build.js" + "build": "node build.js", + "test": "node test/integration.test.js" }, "engines": { "node": ">=10" @@ -14,5 +15,9 @@ "klaw": "^4.1.0", "sharp": "^0.33.5", "yargs": "^17.7.2" + }, + "devDependencies": { + "node-factorio-api": "^0.3.8", + "tar": "^6.2.0" } } diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..f42ab88 --- /dev/null +++ b/test/README.md @@ -0,0 +1,49 @@ +# Integration Tests for Subspace Storage + +This directory contains integration tests for the Subspace Storage mod. + +## How it works + +The integration tests: + +1. Download a headless version of Factorio based on the version specified +2. Find the appropriate mod zip file in the `dist/` directory for the Factorio version +3. Create a dummy `clusterio_lib` mod to satisfy dependencies +4. Start Factorio with the mod and check if it loads without crashing + +## Running tests locally + +First, build the mod: + +```bash +npm run build +``` + +Then run the integration tests: + +```bash +npm test +``` + +You can specify a specific Factorio version to test with: + +```bash +FACTORIO_VERSION=2.0.39 npm test +``` + +You can also specify a specific mod version to test with: + +```bash +MOD_VERSION=2.1.20 npm test +``` + +## Test Matrix in CI + +In the GitHub Actions workflow, we run a test matrix with the following Factorio versions: + +- 0.17.79 +- 1.0.0 +- 1.1.100 +- 2.0.39 + +These match the respective Factorio versions that the mod supports (0.17, 1.0, 1.1, 2.0). diff --git a/test/integration.test.js b/test/integration.test.js new file mode 100644 index 0000000..aaec490 --- /dev/null +++ b/test/integration.test.js @@ -0,0 +1,210 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require('fs-extra'); +const path = require('path'); +const https = require('https'); +const { createWriteStream } = require('fs'); +const { spawn } = require('child_process'); +const tar = require('tar'); + +// Factorio version to test with +const factorioVersion = process.env.FACTORIO_VERSION || '2.0.39'; +const modVersion = process.env.MOD_VERSION; + +const downloadFactorio = async () => { + const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); + const tarballPath = path.join(__dirname, `factorio_${factorioVersion}.tar.xz`); + const downloadUrl = `https://factorio.com/get-download/${factorioVersion}/headless/linux64`; + + console.log(`Downloading Factorio ${factorioVersion} from ${downloadUrl}`); + + // The factorio executable will be in baseDir/factorio/bin/... + const factorioDir = path.join(baseDir, 'factorio'); + + if (await fs.pathExists(factorioDir)) { + console.log('Factorio directory already exists, skipping download'); + return factorioDir; + } + + await fs.ensureDir(baseDir); + + // Download the tarball + await new Promise((resolve, reject) => { + const file = createWriteStream(tarballPath); + https.get(downloadUrl, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Follow redirect + https.get(response.headers.location, (redirectResponse) => { + redirectResponse.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', reject); + } else { + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + } + }).on('error', reject); + }); + + console.log('Download complete, extracting...'); + + // Extract to the base directory - the archive already contains a 'factorio' folder + await new Promise((resolve, reject) => { + const extract = spawn('tar', ['xf', tarballPath, '-C', baseDir]); + + extract.stdout?.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + extract.stderr?.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + extract.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`tar extraction failed with code ${code}`)); + } + }); + }); + + // Clean up the tarball + await fs.unlink(tarballPath); + + console.log('Extraction complete'); + return factorioDir; +}; + +const findModFile = async () => { + const distDir = path.join(__dirname, '..', 'dist'); + const files = await fs.readdir(distDir); + + let modFile; + if (modVersion) { + // Look for a specific version if specified + modFile = files.find(file => file === `subspace_storage_${modVersion}.zip`); + } else { + // Get info.json to find all versions + const infoJson = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'src', 'info.json'))); + + // Find the variant that corresponds to our factorio version + const variant = infoJson.variants.find(v => v.factorio_version === factorioVersion.split('.').slice(0, 2).join('.')); + + if (!variant) { + throw new Error(`No mod variant found for Factorio ${factorioVersion}`); + } + + modFile = files.find(file => file === `subspace_storage_${variant.version}.zip`); + } + + if (!modFile) { + throw new Error('Could not find mod file in dist directory'); + } + + return path.join(distDir, modFile); +}; + +const setupModDirectory = async (factorioDir) => { + const modDir = path.join(factorioDir, 'mods'); + await fs.ensureDir(modDir); + + // Find the mod file + const modFile = await findModFile(); + console.log(`Using mod file: ${modFile}`); + + // Copy the mod file to the mods directory + await fs.copy(modFile, path.join(modDir, path.basename(modFile))); + + // For dependency testing, we need to provide clusterio_lib + // This is a simplified test so we'll create a dummy mod + const clusterioLibDir = path.join(modDir, 'clusterio_lib'); + await fs.ensureDir(clusterioLibDir); + + // Create a minimal info.json for the dummy mod + await fs.writeFile(path.join(clusterioLibDir, 'info.json'), JSON.stringify({ + name: "clusterio_lib", + version: "1.0.0", + title: "Clusterio Library", + author: "Clusterio Team", + factorio_version: factorioVersion.split('.').slice(0, 2).join('.'), + dependencies: ["base >= 0.17.0"] + }, null, 4)); + + console.log(`Mods directory set up at: ${modDir}`); + return modDir; +}; + +const runFactorio = async (factorioDir) => { + console.log('Starting Factorio to test mod loading...'); + + // Get the actual executable path - factorioDir is already the 'factorio' subdirectory + const factorioBin = path.join(factorioDir, 'bin', 'x64', 'factorio'); + console.log(`Factorio executable path: ${factorioBin}`); + + return new Promise((resolve, reject) => { + const factorio = spawn(factorioBin, [ + '--create', './test-map', + '--mod-directory', path.join(factorioDir, 'mods'), + '--benchmark', '1', // Run for a short time then exit + '--benchmark-ticks', '1', + '--no-log-rotation' + ]); + + let output = ''; + let errorOutput = ''; + + factorio.stdout.on('data', (data) => { + const text = data.toString(); + output += text; + process.stdout.write(text); + }); + + factorio.stderr.on('data', (data) => { + const text = data.toString(); + errorOutput += text; + process.stderr.write(text); + }); + + factorio.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Factorio exited with code ${code}: ${errorOutput}`)); + return; + } + + // Check for critical errors in the output + if (output.includes('Error') || output.includes('EXCEPTION')) { + if ( + output.includes('Error while loading mods') || + output.includes('subspace_storage') + ) { + reject(new Error(`Mod loading failed: ${output}`)); + return; + } + } + + resolve(); + }); + }); +}; + +const main = async () => { + try { + const factorioDir = await downloadFactorio(); + await setupModDirectory(factorioDir); + await runFactorio(factorioDir); + console.log('Integration test passed successfully!'); + process.exit(0); + } catch (error) { + console.error('Integration test failed:', error); + process.exit(1); + } +}; + +main(); From cce953033806db952a4ffa80c7bfe8da1d846f95 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 20:04:53 +0100 Subject: [PATCH 02/11] Automatically download clusterio_lib from gh actions --- .github/workflows/integration-tests.yml | 6 + README.md | 29 ++++ package.json | 2 +- test/integration.test.js | 216 ++++++++++++++++++++++-- 4 files changed, 238 insertions(+), 15 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 06f43ec..72f7c3e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -7,6 +7,11 @@ on: branches: [ main, master ] workflow_dispatch: +# Define permissions needed for the GITHUB_TOKEN +permissions: + contents: read + actions: read + jobs: build: runs-on: ubuntu-latest @@ -62,4 +67,5 @@ jobs: - name: Run integration test env: FACTORIO_VERSION: ${{ matrix.factorio-version }} + GITHUB_TOKEN: ${{ secrets.GH_PAT }} run: node test/integration.test.js diff --git a/README.md b/README.md index dabfb45..f4dab5d 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,32 @@ Build Instructions Install Node.js version 10 or later and run `npm install` then run `node build`. It will output the built mod into the `dist` folder by default. See `node build --help` for options. + +Testing +------------------ + +To run the integration test: + +```bash +# Basic test using a dummy clusterio_lib mod +npm test + +# Test with the latest clusterio_lib from GitHub Actions +GITHUB_TOKEN=your_github_token npm test +``` + +The integration test will: +1. Download and set up a Factorio headless server +2. Build and install the Subspace Storage mod +3. When provided with a GitHub token, download the latest clusterio_lib from GitHub Actions +4. Run Factorio to verify that the mod loads correctly + +Note: To download the clusterio_lib mod from GitHub Actions, you need a GitHub personal access token with the `public_repo` scope. If not provided, the test will fall back to using a dummy clusterio_lib mod. + +### CI Configuration + +For CI environments, you should store the GitHub token as a secret: + +1. Go to your repository Settings → Secrets and variables → Actions +2. Add a new repository secret named `GH_PAT` with your GitHub personal access token +3. The CI workflow is already configured to use this secret when running tests diff --git a/package.json b/package.json index 01c240d..7dd80ab 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ "jszip": "^3.10.1", "klaw": "^4.1.0", "sharp": "^0.33.5", + "unzipper": "^0.10.14", "yargs": "^17.7.2" }, "devDependencies": { - "node-factorio-api": "^0.3.8", "tar": "^6.2.0" } } diff --git a/test/integration.test.js b/test/integration.test.js index aaec490..56913cc 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -7,10 +7,14 @@ const https = require('https'); const { createWriteStream } = require('fs'); const { spawn } = require('child_process'); const tar = require('tar'); +const { pipeline } = require('stream/promises'); +const { Extract } = require('unzipper'); // Factorio version to test with const factorioVersion = process.env.FACTORIO_VERSION || '2.0.39'; const modVersion = process.env.MOD_VERSION; +// GitHub token for API access (can be passed as environment variable) +const githubToken = process.env.GITHUB_TOKEN; const downloadFactorio = async () => { const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); @@ -111,6 +115,197 @@ const findModFile = async () => { return path.join(distDir, modFile); }; +const downloadClusterioLib = async (modDir) => { + console.log('Downloading clusterio_lib from GitHub Actions...'); + + if (!githubToken) { + throw new Error('GITHUB_TOKEN environment variable is required to download artifacts from GitHub Actions'); + } + + // First, get the latest workflow run ID + const getLatestRunId = async () => { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/repos/clusterio/clusterio/actions/runs?status=success&per_page=1', + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`Failed to get latest workflow run: ${res.statusCode}, ${data}`)); + return; + } + + try { + const response = JSON.parse(data); + if (!response.workflow_runs || response.workflow_runs.length === 0) { + reject(new Error('No workflow runs found')); + return; + } + + resolve(response.workflow_runs[0].id); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', reject); + req.end(); + }); + }; + + // Get the artifact ID for clusterio_lib + const getArtifactId = async (runId) => { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: `/repos/clusterio/clusterio/actions/runs/${runId}/artifacts`, + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`Failed to get artifacts: ${res.statusCode}, ${data}`)); + return; + } + + try { + const response = JSON.parse(data); + const artifact = response.artifacts.find(a => a.name === 'clusterio_lib'); + if (!artifact) { + reject(new Error('No clusterio_lib artifact found')); + return; + } + + resolve(artifact.id); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', reject); + req.end(); + }); + }; + + // Download the artifact + const downloadArtifact = async (artifactId) => { + const artifactZipPath = path.join(__dirname, 'clusterio_lib_artifact.zip'); + + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: `/repos/clusterio/clusterio/actions/artifacts/${artifactId}/zip`, + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + if (res.statusCode === 302) { + // Follow redirect + const file = createWriteStream(artifactZipPath); + https.get(res.headers.location, (redirectRes) => { + redirectRes.pipe(file); + file.on('finish', () => { + file.close(); + resolve(artifactZipPath); + }); + }).on('error', (err) => { + fs.unlink(artifactZipPath, () => {}); + reject(err); + }); + } else { + reject(new Error(`Expected redirect, got ${res.statusCode}`)); + } + }); + + req.on('error', reject); + req.end(); + }); + }; + + try { + // Step 1: Get the latest workflow run ID + const runId = await getLatestRunId(); + console.log(`Found latest successful workflow run: ${runId}`); + + // Step 2: Get the artifact ID + const artifactId = await getArtifactId(runId); + console.log(`Found clusterio_lib artifact: ${artifactId}`); + + // Step 3: Download the artifact + const artifactZipPath = await downloadArtifact(artifactId); + console.log(`Downloaded artifact to: ${artifactZipPath}`); + + // Step 4: Extract the artifact + const extractDir = path.join(__dirname, 'clusterio_lib_extract'); + await fs.ensureDir(extractDir); + + await pipeline( + fs.createReadStream(artifactZipPath), + Extract({ path: extractDir }) + ); + + // Add delay to allow extraction to properly complete before reading the directory + await new Promise(r => setTimeout(r, 500)); + + // Step 5: Extract all files starting with clusterio_lib_ (older versions just won't be loaded by factorio) + const files = await fs.readdir(extractDir); + const modFiles = files.filter(file => file.startsWith('clusterio_lib_') && file.endsWith('.zip')); + + if (modFiles.length === 0) { + throw new Error('Could not find clusterio_lib mod file in downloaded artifact'); + } + + // Copy the mod file to the mods directory + for (const modFile of modFiles) { + await fs.copy( + path.join(extractDir, modFile), + path.join(modDir, modFile) + ); + } + + // Clean up + await fs.remove(artifactZipPath); + await fs.remove(extractDir); + + console.log(`Successfully installed clusterio_lib mod:`, modFiles); + } catch (error) { + throw new Error(`Failed to download clusterio_lib: ${error.message}`); + } +}; + const setupModDirectory = async (factorioDir) => { const modDir = path.join(factorioDir, 'mods'); await fs.ensureDir(modDir); @@ -122,20 +317,13 @@ const setupModDirectory = async (factorioDir) => { // Copy the mod file to the mods directory await fs.copy(modFile, path.join(modDir, path.basename(modFile))); - // For dependency testing, we need to provide clusterio_lib - // This is a simplified test so we'll create a dummy mod - const clusterioLibDir = path.join(modDir, 'clusterio_lib'); - await fs.ensureDir(clusterioLibDir); - - // Create a minimal info.json for the dummy mod - await fs.writeFile(path.join(clusterioLibDir, 'info.json'), JSON.stringify({ - name: "clusterio_lib", - version: "1.0.0", - title: "Clusterio Library", - author: "Clusterio Team", - factorio_version: factorioVersion.split('.').slice(0, 2).join('.'), - dependencies: ["base >= 0.17.0"] - }, null, 4)); + // Download and install the real clusterio_lib from GitHub Actions + if (!githubToken) { + console.warn('GITHUB_TOKEN not provided, unable to download clusterio_lib from GitHub Actions'); + throw new Error('GITHUB_TOKEN not provided'); + } + + await downloadClusterioLib(modDir); console.log(`Mods directory set up at: ${modDir}`); return modDir; From c6ae2fa9ae1e2869b5655c0c449f89f86c75a23e Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 20:12:35 +0100 Subject: [PATCH 03/11] Check that the mod gets loaded --- test/integration.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration.test.js b/test/integration.test.js index 56913cc..3f40ab2 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -377,6 +377,12 @@ const runFactorio = async (factorioDir) => { } } + // Check that the mod was loaded correctly + if (!output.includes('Loading mod subspace_storage')) { + reject(new Error('Could not find "Loading mod subspace_storage" in the output. The mod may not have been loaded correctly.')); + return; + } + resolve(); }); }); From 73b5b6a1b7a49cf0bda55d0e42ff3ec01336f18a Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 20:23:47 +0100 Subject: [PATCH 04/11] Log mods --- test/integration.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/integration.test.js b/test/integration.test.js index 3f40ab2..21778fc 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -335,6 +335,9 @@ const runFactorio = async (factorioDir) => { // Get the actual executable path - factorioDir is already the 'factorio' subdirectory const factorioBin = path.join(factorioDir, 'bin', 'x64', 'factorio'); console.log(`Factorio executable path: ${factorioBin}`); + + // Mods: + console.log("Mods:", await fs.readdir(path.join(factorioDir, 'mods'))); return new Promise((resolve, reject) => { const factorio = spawn(factorioBin, [ From 796192388c0061f9c016c4c55a30a3d14f2823d0 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 20:51:13 +0100 Subject: [PATCH 05/11] Only use the required version of clusterio lib --- test/integration.test.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/integration.test.js b/test/integration.test.js index 21778fc..203406d 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -11,11 +11,30 @@ const { pipeline } = require('stream/promises'); const { Extract } = require('unzipper'); // Factorio version to test with -const factorioVersion = process.env.FACTORIO_VERSION || '2.0.39'; +const factorioVersion = process.env.FACTORIO_VERSION || '1.1.110'; const modVersion = process.env.MOD_VERSION; // GitHub token for API access (can be passed as environment variable) const githubToken = process.env.GITHUB_TOKEN; +const checkModVersionAgainstFactorioVersion = (modVersion, factorioVersion) => { + const modVersionParts = modVersion.split('.'); + const factorioVersionParts = factorioVersion.split('.'); + + switch (modVersionParts[2]) { + case '20': + return factorioVersionParts[0] === '2'; + case '11': + return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '1'; + case '10': + return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '0'; + case '17': + return factorioVersionParts[0] === '0' && factorioVersionParts[1] === '17'; + default: + return false; + } +}; + + const downloadFactorio = async () => { const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); const tarballPath = path.join(__dirname, `factorio_${factorioVersion}.tar.xz`); @@ -283,13 +302,19 @@ const downloadClusterioLib = async (modDir) => { // Step 5: Extract all files starting with clusterio_lib_ (older versions just won't be loaded by factorio) const files = await fs.readdir(extractDir); const modFiles = files.filter(file => file.startsWith('clusterio_lib_') && file.endsWith('.zip')); + const validModFiles = modFiles.filter(file => checkModVersionAgainstFactorioVersion( + file + .replace('clusterio_lib_', '') + .replace('.zip', ''), + factorioVersion + )); - if (modFiles.length === 0) { + if (validModFiles.length === 0) { throw new Error('Could not find clusterio_lib mod file in downloaded artifact'); } // Copy the mod file to the mods directory - for (const modFile of modFiles) { + for (const modFile of validModFiles) { await fs.copy( path.join(extractDir, modFile), path.join(modDir, modFile) @@ -300,7 +325,7 @@ const downloadClusterioLib = async (modDir) => { await fs.remove(artifactZipPath); await fs.remove(extractDir); - console.log(`Successfully installed clusterio_lib mod:`, modFiles); + console.log(`Successfully installed clusterio_lib mod:`, validModFiles); } catch (error) { throw new Error(`Failed to download clusterio_lib: ${error.message}`); } From 44a73ef69602a440e9cd15b864caed5852c5caf8 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 20:55:55 +0100 Subject: [PATCH 06/11] Handle .18, use tabs --- test/integration.test.js | 783 ++++++++++++++++++++------------------- 1 file changed, 392 insertions(+), 391 deletions(-) diff --git a/test/integration.test.js b/test/integration.test.js index 203406d..9f0ff19 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -17,416 +17,417 @@ const modVersion = process.env.MOD_VERSION; const githubToken = process.env.GITHUB_TOKEN; const checkModVersionAgainstFactorioVersion = (modVersion, factorioVersion) => { - const modVersionParts = modVersion.split('.'); - const factorioVersionParts = factorioVersion.split('.'); - - switch (modVersionParts[2]) { - case '20': - return factorioVersionParts[0] === '2'; - case '11': - return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '1'; - case '10': - return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '0'; - case '17': - return factorioVersionParts[0] === '0' && factorioVersionParts[1] === '17'; - default: - return false; - } + const modVersionParts = modVersion.split('.'); + const factorioVersionParts = factorioVersion.split('.'); + + switch (modVersionParts[2]) { + case '20': + return factorioVersionParts[0] === '2'; + case '11': + return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '1'; + case '10', '18': + return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '0'; + case '17': + return factorioVersionParts[0] === '0' && factorioVersionParts[1] === '17'; + default: + return false; + } }; const downloadFactorio = async () => { - const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); - const tarballPath = path.join(__dirname, `factorio_${factorioVersion}.tar.xz`); - const downloadUrl = `https://factorio.com/get-download/${factorioVersion}/headless/linux64`; - - console.log(`Downloading Factorio ${factorioVersion} from ${downloadUrl}`); - - // The factorio executable will be in baseDir/factorio/bin/... - const factorioDir = path.join(baseDir, 'factorio'); - - if (await fs.pathExists(factorioDir)) { - console.log('Factorio directory already exists, skipping download'); - return factorioDir; - } - - await fs.ensureDir(baseDir); - - // Download the tarball - await new Promise((resolve, reject) => { - const file = createWriteStream(tarballPath); - https.get(downloadUrl, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - // Follow redirect - https.get(response.headers.location, (redirectResponse) => { - redirectResponse.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', reject); - } else { - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - } - }).on('error', reject); - }); - - console.log('Download complete, extracting...'); - - // Extract to the base directory - the archive already contains a 'factorio' folder - await new Promise((resolve, reject) => { - const extract = spawn('tar', ['xf', tarballPath, '-C', baseDir]); - - extract.stdout?.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - extract.stderr?.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - extract.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`tar extraction failed with code ${code}`)); - } - }); - }); - - // Clean up the tarball - await fs.unlink(tarballPath); - - console.log('Extraction complete'); - return factorioDir; + const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); + const tarballPath = path.join(__dirname, `factorio_${factorioVersion}.tar.xz`); + const downloadUrl = `https://factorio.com/get-download/${factorioVersion}/headless/linux64`; + + console.log(`Downloading Factorio ${factorioVersion} from ${downloadUrl}`); + + // The factorio executable will be in baseDir/factorio/bin/... + const factorioDir = path.join(baseDir, 'factorio'); + + if (await fs.pathExists(factorioDir)) { + console.log('Factorio directory already exists, skipping download'); + return factorioDir; + } + + await fs.ensureDir(baseDir); + + // Download the tarball + await new Promise((resolve, reject) => { + const file = createWriteStream(tarballPath); + https.get(downloadUrl, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Follow redirect + https.get(response.headers.location, (redirectResponse) => { + redirectResponse.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', reject); + } else { + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + } + }).on('error', reject); + }); + + console.log('Download complete, extracting...'); + + // Extract to the base directory - the archive already contains a 'factorio' folder + await new Promise((resolve, reject) => { + const extract = spawn('tar', ['xf', tarballPath, '-C', baseDir]); + + extract.stdout?.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + extract.stderr?.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + extract.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`tar extraction failed with code ${code}`)); + } + }); + }); + + // Clean up the tarball + await fs.unlink(tarballPath); + + console.log('Extraction complete'); + return factorioDir; }; const findModFile = async () => { - const distDir = path.join(__dirname, '..', 'dist'); - const files = await fs.readdir(distDir); - - let modFile; - if (modVersion) { - // Look for a specific version if specified - modFile = files.find(file => file === `subspace_storage_${modVersion}.zip`); - } else { - // Get info.json to find all versions - const infoJson = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'src', 'info.json'))); - - // Find the variant that corresponds to our factorio version - const variant = infoJson.variants.find(v => v.factorio_version === factorioVersion.split('.').slice(0, 2).join('.')); - - if (!variant) { - throw new Error(`No mod variant found for Factorio ${factorioVersion}`); - } - - modFile = files.find(file => file === `subspace_storage_${variant.version}.zip`); - } - - if (!modFile) { - throw new Error('Could not find mod file in dist directory'); - } - - return path.join(distDir, modFile); + const distDir = path.join(__dirname, '..', 'dist'); + const files = await fs.readdir(distDir); + + let modFile; + if (modVersion) { + // Look for a specific version if specified + modFile = files.find(file => file === `subspace_storage_${modVersion}.zip`); + } else { + // Get info.json to find all versions + const infoJson = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'src', 'info.json'))); + + // Find the variant that corresponds to our factorio version + const variant = infoJson.variants.find(v => v.factorio_version === factorioVersion.split('.').slice(0, 2).join('.')); + + if (!variant) { + throw new Error(`No mod variant found for Factorio ${factorioVersion}`); + } + + modFile = files.find(file => file === `subspace_storage_${variant.version}.zip`); + } + + if (!modFile) { + throw new Error('Could not find mod file in dist directory'); + } + + return path.join(distDir, modFile); }; const downloadClusterioLib = async (modDir) => { - console.log('Downloading clusterio_lib from GitHub Actions...'); - - if (!githubToken) { - throw new Error('GITHUB_TOKEN environment variable is required to download artifacts from GitHub Actions'); - } - - // First, get the latest workflow run ID - const getLatestRunId = async () => { - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: '/repos/clusterio/clusterio/actions/runs?status=success&per_page=1', - method: 'GET', - headers: { - 'User-Agent': 'subspace-storage-integration-test', - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json' - } - }; - - const req = https.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - reject(new Error(`Failed to get latest workflow run: ${res.statusCode}, ${data}`)); - return; - } - - try { - const response = JSON.parse(data); - if (!response.workflow_runs || response.workflow_runs.length === 0) { - reject(new Error('No workflow runs found')); - return; - } - - resolve(response.workflow_runs[0].id); - } catch (error) { - reject(error); - } - }); - }); - - req.on('error', reject); - req.end(); - }); - }; - - // Get the artifact ID for clusterio_lib - const getArtifactId = async (runId) => { - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: `/repos/clusterio/clusterio/actions/runs/${runId}/artifacts`, - method: 'GET', - headers: { - 'User-Agent': 'subspace-storage-integration-test', - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json' - } - }; - - const req = https.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - reject(new Error(`Failed to get artifacts: ${res.statusCode}, ${data}`)); - return; - } - - try { - const response = JSON.parse(data); - const artifact = response.artifacts.find(a => a.name === 'clusterio_lib'); - if (!artifact) { - reject(new Error('No clusterio_lib artifact found')); - return; - } - - resolve(artifact.id); - } catch (error) { - reject(error); - } - }); - }); - - req.on('error', reject); - req.end(); - }); - }; - - // Download the artifact - const downloadArtifact = async (artifactId) => { - const artifactZipPath = path.join(__dirname, 'clusterio_lib_artifact.zip'); - - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: `/repos/clusterio/clusterio/actions/artifacts/${artifactId}/zip`, - method: 'GET', - headers: { - 'User-Agent': 'subspace-storage-integration-test', - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json' - } - }; - - const req = https.request(options, (res) => { - if (res.statusCode === 302) { - // Follow redirect - const file = createWriteStream(artifactZipPath); - https.get(res.headers.location, (redirectRes) => { - redirectRes.pipe(file); - file.on('finish', () => { - file.close(); - resolve(artifactZipPath); - }); - }).on('error', (err) => { - fs.unlink(artifactZipPath, () => {}); - reject(err); - }); - } else { - reject(new Error(`Expected redirect, got ${res.statusCode}`)); - } - }); - - req.on('error', reject); - req.end(); - }); - }; - - try { - // Step 1: Get the latest workflow run ID - const runId = await getLatestRunId(); - console.log(`Found latest successful workflow run: ${runId}`); - - // Step 2: Get the artifact ID - const artifactId = await getArtifactId(runId); - console.log(`Found clusterio_lib artifact: ${artifactId}`); - - // Step 3: Download the artifact - const artifactZipPath = await downloadArtifact(artifactId); - console.log(`Downloaded artifact to: ${artifactZipPath}`); - - // Step 4: Extract the artifact - const extractDir = path.join(__dirname, 'clusterio_lib_extract'); - await fs.ensureDir(extractDir); - - await pipeline( - fs.createReadStream(artifactZipPath), - Extract({ path: extractDir }) - ); - - // Add delay to allow extraction to properly complete before reading the directory - await new Promise(r => setTimeout(r, 500)); - - // Step 5: Extract all files starting with clusterio_lib_ (older versions just won't be loaded by factorio) - const files = await fs.readdir(extractDir); - const modFiles = files.filter(file => file.startsWith('clusterio_lib_') && file.endsWith('.zip')); - const validModFiles = modFiles.filter(file => checkModVersionAgainstFactorioVersion( - file - .replace('clusterio_lib_', '') - .replace('.zip', ''), - factorioVersion - )); - - if (validModFiles.length === 0) { - throw new Error('Could not find clusterio_lib mod file in downloaded artifact'); - } - - // Copy the mod file to the mods directory - for (const modFile of validModFiles) { - await fs.copy( - path.join(extractDir, modFile), - path.join(modDir, modFile) - ); - } - - // Clean up - await fs.remove(artifactZipPath); - await fs.remove(extractDir); - - console.log(`Successfully installed clusterio_lib mod:`, validModFiles); - } catch (error) { - throw new Error(`Failed to download clusterio_lib: ${error.message}`); - } + console.log('Downloading clusterio_lib from GitHub Actions...'); + + if (!githubToken) { + throw new Error('GITHUB_TOKEN environment variable is required to download artifacts from GitHub Actions'); + } + + // First, get the latest workflow run ID + const getLatestRunId = async () => { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/repos/clusterio/clusterio/actions/runs?status=success&per_page=1', + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`Failed to get latest workflow run: ${res.statusCode}, ${data}`)); + return; + } + + try { + const response = JSON.parse(data); + if (!response.workflow_runs || response.workflow_runs.length === 0) { + reject(new Error('No workflow runs found')); + return; + } + + resolve(response.workflow_runs[0].id); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', reject); + req.end(); + }); + }; + + // Get the artifact ID for clusterio_lib + const getArtifactId = async (runId) => { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: `/repos/clusterio/clusterio/actions/runs/${runId}/artifacts`, + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`Failed to get artifacts: ${res.statusCode}, ${data}`)); + return; + } + + try { + const response = JSON.parse(data); + const artifact = response.artifacts.find(a => a.name === 'clusterio_lib'); + if (!artifact) { + reject(new Error('No clusterio_lib artifact found')); + return; + } + + resolve(artifact.id); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', reject); + req.end(); + }); + }; + + // Download the artifact + const downloadArtifact = async (artifactId) => { + const artifactZipPath = path.join(__dirname, 'clusterio_lib_artifact.zip'); + + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: `/repos/clusterio/clusterio/actions/artifacts/${artifactId}/zip`, + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + if (res.statusCode === 302) { + // Follow redirect + const file = createWriteStream(artifactZipPath); + https.get(res.headers.location, (redirectRes) => { + redirectRes.pipe(file); + file.on('finish', () => { + file.close(); + resolve(artifactZipPath); + }); + }).on('error', (err) => { + fs.unlink(artifactZipPath, () => { }); + reject(err); + }); + } else { + reject(new Error(`Expected redirect, got ${res.statusCode}`)); + } + }); + + req.on('error', reject); + req.end(); + }); + }; + + try { + // Step 1: Get the latest workflow run ID + const runId = await getLatestRunId(); + console.log(`Found latest successful workflow run: ${runId}`); + + // Step 2: Get the artifact ID + const artifactId = await getArtifactId(runId); + console.log(`Found clusterio_lib artifact: ${artifactId}`); + + // Step 3: Download the artifact + const artifactZipPath = await downloadArtifact(artifactId); + console.log(`Downloaded artifact to: ${artifactZipPath}`); + + // Step 4: Extract the artifact + const extractDir = path.join(__dirname, 'clusterio_lib_extract'); + await fs.ensureDir(extractDir); + + await pipeline( + fs.createReadStream(artifactZipPath), + Extract({ path: extractDir }) + ); + + // Add delay to allow extraction to properly complete before reading the directory + await new Promise(r => setTimeout(r, 500)); + + // Step 5: Extract all files starting with clusterio_lib_ (older versions just won't be loaded by factorio) + const files = await fs.readdir(extractDir); + const modFiles = files.filter(file => file.startsWith('clusterio_lib_') && file.endsWith('.zip')); + console.log("Mods in artifact:", modFiles); + const validModFiles = modFiles.filter(file => checkModVersionAgainstFactorioVersion( + file + .replace('clusterio_lib_', '') + .replace('.zip', ''), + factorioVersion + )); + + if (validModFiles.length === 0) { + throw new Error('Could not find clusterio_lib mod file in downloaded artifact'); + } + + // Copy the mod file to the mods directory + for (const modFile of validModFiles) { + await fs.copy( + path.join(extractDir, modFile), + path.join(modDir, modFile) + ); + } + + // Clean up + await fs.remove(artifactZipPath); + await fs.remove(extractDir); + + console.log(`Successfully installed clusterio_lib mod:`, validModFiles); + } catch (error) { + throw new Error(`Failed to download clusterio_lib: ${error.message}`); + } }; const setupModDirectory = async (factorioDir) => { - const modDir = path.join(factorioDir, 'mods'); - await fs.ensureDir(modDir); - - // Find the mod file - const modFile = await findModFile(); - console.log(`Using mod file: ${modFile}`); - - // Copy the mod file to the mods directory - await fs.copy(modFile, path.join(modDir, path.basename(modFile))); - - // Download and install the real clusterio_lib from GitHub Actions - if (!githubToken) { - console.warn('GITHUB_TOKEN not provided, unable to download clusterio_lib from GitHub Actions'); - throw new Error('GITHUB_TOKEN not provided'); - } - - await downloadClusterioLib(modDir); - - console.log(`Mods directory set up at: ${modDir}`); - return modDir; + const modDir = path.join(factorioDir, 'mods'); + await fs.ensureDir(modDir); + + // Find the mod file + const modFile = await findModFile(); + console.log(`Using mod file: ${modFile}`); + + // Copy the mod file to the mods directory + await fs.copy(modFile, path.join(modDir, path.basename(modFile))); + + // Download and install the real clusterio_lib from GitHub Actions + if (!githubToken) { + console.warn('GITHUB_TOKEN not provided, unable to download clusterio_lib from GitHub Actions'); + throw new Error('GITHUB_TOKEN not provided'); + } + + await downloadClusterioLib(modDir); + + console.log(`Mods directory set up at: ${modDir}`); + return modDir; }; const runFactorio = async (factorioDir) => { - console.log('Starting Factorio to test mod loading...'); - - // Get the actual executable path - factorioDir is already the 'factorio' subdirectory - const factorioBin = path.join(factorioDir, 'bin', 'x64', 'factorio'); - console.log(`Factorio executable path: ${factorioBin}`); - - // Mods: - console.log("Mods:", await fs.readdir(path.join(factorioDir, 'mods'))); - - return new Promise((resolve, reject) => { - const factorio = spawn(factorioBin, [ - '--create', './test-map', - '--mod-directory', path.join(factorioDir, 'mods'), - '--benchmark', '1', // Run for a short time then exit - '--benchmark-ticks', '1', - '--no-log-rotation' - ]); - - let output = ''; - let errorOutput = ''; - - factorio.stdout.on('data', (data) => { - const text = data.toString(); - output += text; - process.stdout.write(text); - }); - - factorio.stderr.on('data', (data) => { - const text = data.toString(); - errorOutput += text; - process.stderr.write(text); - }); - - factorio.on('close', (code) => { - if (code !== 0) { - reject(new Error(`Factorio exited with code ${code}: ${errorOutput}`)); - return; - } - - // Check for critical errors in the output - if (output.includes('Error') || output.includes('EXCEPTION')) { - if ( - output.includes('Error while loading mods') || - output.includes('subspace_storage') - ) { - reject(new Error(`Mod loading failed: ${output}`)); - return; - } - } - - // Check that the mod was loaded correctly - if (!output.includes('Loading mod subspace_storage')) { - reject(new Error('Could not find "Loading mod subspace_storage" in the output. The mod may not have been loaded correctly.')); - return; - } - - resolve(); - }); - }); + console.log('Starting Factorio to test mod loading...'); + + // Get the actual executable path - factorioDir is already the 'factorio' subdirectory + const factorioBin = path.join(factorioDir, 'bin', 'x64', 'factorio'); + console.log(`Factorio executable path: ${factorioBin}`); + + // Mods: + console.log("Mods:", await fs.readdir(path.join(factorioDir, 'mods'))); + + return new Promise((resolve, reject) => { + const factorio = spawn(factorioBin, [ + '--create', './test-map', + '--mod-directory', path.join(factorioDir, 'mods'), + '--benchmark', '1', // Run for a short time then exit + '--benchmark-ticks', '1', + '--no-log-rotation' + ]); + + let output = ''; + let errorOutput = ''; + + factorio.stdout.on('data', (data) => { + const text = data.toString(); + output += text; + process.stdout.write(text); + }); + + factorio.stderr.on('data', (data) => { + const text = data.toString(); + errorOutput += text; + process.stderr.write(text); + }); + + factorio.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Factorio exited with code ${code}: ${errorOutput}`)); + return; + } + + // Check for critical errors in the output + if (output.includes('Error') || output.includes('EXCEPTION')) { + if ( + output.includes('Error while loading mods') || + output.includes('subspace_storage') + ) { + reject(new Error(`Mod loading failed: ${output}`)); + return; + } + } + + // Check that the mod was loaded correctly + if (!output.includes('Loading mod subspace_storage')) { + reject(new Error('Could not find "Loading mod subspace_storage" in the output. The mod may not have been loaded correctly.')); + return; + } + + resolve(); + }); + }); }; const main = async () => { - try { - const factorioDir = await downloadFactorio(); - await setupModDirectory(factorioDir); - await runFactorio(factorioDir); - console.log('Integration test passed successfully!'); - process.exit(0); - } catch (error) { - console.error('Integration test failed:', error); - process.exit(1); - } + try { + const factorioDir = await downloadFactorio(); + await setupModDirectory(factorioDir); + await runFactorio(factorioDir); + console.log('Integration test passed successfully!'); + process.exit(0); + } catch (error) { + console.error('Integration test failed:', error); + process.exit(1); + } }; main(); From 7d5dcb864a05c06b6e2f38ad5c179df98949d2e2 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 11 Mar 2025 21:07:49 +0100 Subject: [PATCH 07/11] Merge workflow --- .github/workflows/integration-tests.yml | 71 ------------------------- .github/workflows/node.js.yml | 43 +++++++++++++++ 2 files changed, 43 insertions(+), 71 deletions(-) delete mode 100644 .github/workflows/integration-tests.yml diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml deleted file mode 100644 index 72f7c3e..0000000 --- a/.github/workflows/integration-tests.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Integration Tests - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - workflow_dispatch: - -# Define permissions needed for the GITHUB_TOKEN -permissions: - contents: read - actions: read - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 22.x - - run: pnpm i --no-frozen-lockfile - - run: node build.js - # Upload build to artifacts - - name: Upload build to artifacts - uses: actions/upload-artifact@v4 - with: - name: subspace_storage-builds - path: ./dist - if-no-files-found: error - - test: - needs: build - runs-on: ubuntu-latest - strategy: - matrix: - factorio-version: ['0.17.79', '1.0.0', '1.1.110', '2.0.39'] - fail-fast: false - - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 22.x - - # Install xz-utils for tar.xz extraction - - name: Install xz-utils - run: sudo apt-get update && sudo apt-get install -y xz-utils - - # Download the built artifacts - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: subspace_storage-builds - path: ./dist - - - name: Install dependencies - run: pnpm i --no-frozen-lockfile - - - name: Run integration test - env: - FACTORIO_VERSION: ${{ matrix.factorio-version }} - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - run: node test/integration.test.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 8134fe9..686e2ce 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -6,6 +6,12 @@ name: Node.js CI on: push: pull_request: + types: [opened] + +# Define permissions needed for the GITHUB_TOKEN +permissions: + contents: read + actions: read jobs: build: @@ -27,3 +33,40 @@ jobs: name: subspace_storage path: ./dist if-no-files-found: error + + test: + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + factorio-version: ['0.17.79', '1.0.0', '1.1.110', '2.0.39'] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + + # Install xz-utils for tar.xz extraction + - name: Install xz-utils + run: sudo apt-get update && sudo apt-get install -y xz-utils + + # Download the built artifacts + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: subspace_storage + path: ./dist + + - name: Install dependencies + run: pnpm i --no-frozen-lockfile + + - name: Run integration test + env: + FACTORIO_VERSION: ${{ matrix.factorio-version }} + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + run: node test/integration.test.js From 833223f0d743abaa625dc377dd944c4b9e16c9f1 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Fri, 14 Mar 2025 22:15:45 +0100 Subject: [PATCH 08/11] Reorganize test environment setup --- test/README.md | 2 +- test/downloadFactorio.js | 80 +++++++++ test/integration.test.js | 359 +------------------------------------- test/setupModDirectory.js | 278 +++++++++++++++++++++++++++++ 4 files changed, 368 insertions(+), 351 deletions(-) create mode 100644 test/downloadFactorio.js create mode 100644 test/setupModDirectory.js diff --git a/test/README.md b/test/README.md index f42ab88..284b600 100644 --- a/test/README.md +++ b/test/README.md @@ -43,7 +43,7 @@ In the GitHub Actions workflow, we run a test matrix with the following Factorio - 0.17.79 - 1.0.0 -- 1.1.100 +- 1.1.110 - 2.0.39 These match the respective Factorio versions that the mod supports (0.17, 1.0, 1.1, 2.0). diff --git a/test/downloadFactorio.js b/test/downloadFactorio.js new file mode 100644 index 0000000..fa349ae --- /dev/null +++ b/test/downloadFactorio.js @@ -0,0 +1,80 @@ +"use strict"; +const { spawn } = require('child_process'); +const { createWriteStream } = require('fs'); +const fs = require('fs-extra'); +const https = require('https'); +const path = require('path'); + +// Factorio version to test with +const factorioVersion = process.env.FACTORIO_VERSION || '1.1.110'; + +const downloadFactorio = async () => { + const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); + const tarballPath = path.join(__dirname, `factorio_${factorioVersion}.tar.xz`); + const downloadUrl = `https://factorio.com/get-download/${factorioVersion}/headless/linux64`; + + console.log(`Downloading Factorio ${factorioVersion} from ${downloadUrl}`); + + // The factorio executable will be in baseDir/factorio/bin/... + const factorioDir = path.join(baseDir, 'factorio'); + + if (await fs.pathExists(factorioDir)) { + console.log('Factorio directory already exists, skipping download'); + return factorioDir; + } + + await fs.ensureDir(baseDir); + + // Download the tarball + await new Promise((resolve, reject) => { + const file = createWriteStream(tarballPath); + https.get(downloadUrl, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Follow redirect + https.get(response.headers.location, (redirectResponse) => { + redirectResponse.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', reject); + } else { + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + } + }).on('error', reject); + }); + + console.log('Download complete, extracting...'); + + // Extract to the base directory - the archive already contains a 'factorio' folder + await new Promise((resolve, reject) => { + const extract = spawn('tar', ['xf', tarballPath, '-C', baseDir]); + + extract.stdout?.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + extract.stderr?.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + extract.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`tar extraction failed with code ${code}`)); + } + }); + }); + + // Clean up the tarball + await fs.unlink(tarballPath); + + console.log('Extraction complete'); + return factorioDir; +}; +exports.downloadFactorio = downloadFactorio; diff --git a/test/integration.test.js b/test/integration.test.js index 9f0ff19..1cca9e6 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -3,357 +3,16 @@ const fs = require('fs-extra'); const path = require('path'); -const https = require('https'); -const { createWriteStream } = require('fs'); const { spawn } = require('child_process'); -const tar = require('tar'); -const { pipeline } = require('stream/promises'); -const { Extract } = require('unzipper'); - -// Factorio version to test with -const factorioVersion = process.env.FACTORIO_VERSION || '1.1.110'; -const modVersion = process.env.MOD_VERSION; -// GitHub token for API access (can be passed as environment variable) -const githubToken = process.env.GITHUB_TOKEN; - -const checkModVersionAgainstFactorioVersion = (modVersion, factorioVersion) => { - const modVersionParts = modVersion.split('.'); - const factorioVersionParts = factorioVersion.split('.'); - - switch (modVersionParts[2]) { - case '20': - return factorioVersionParts[0] === '2'; - case '11': - return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '1'; - case '10', '18': - return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '0'; - case '17': - return factorioVersionParts[0] === '0' && factorioVersionParts[1] === '17'; - default: - return false; - } -}; - - -const downloadFactorio = async () => { - const baseDir = path.join(__dirname, `factorio_${factorioVersion}`); - const tarballPath = path.join(__dirname, `factorio_${factorioVersion}.tar.xz`); - const downloadUrl = `https://factorio.com/get-download/${factorioVersion}/headless/linux64`; - - console.log(`Downloading Factorio ${factorioVersion} from ${downloadUrl}`); - - // The factorio executable will be in baseDir/factorio/bin/... - const factorioDir = path.join(baseDir, 'factorio'); - - if (await fs.pathExists(factorioDir)) { - console.log('Factorio directory already exists, skipping download'); - return factorioDir; - } - - await fs.ensureDir(baseDir); - - // Download the tarball - await new Promise((resolve, reject) => { - const file = createWriteStream(tarballPath); - https.get(downloadUrl, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - // Follow redirect - https.get(response.headers.location, (redirectResponse) => { - redirectResponse.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', reject); - } else { - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - } - }).on('error', reject); - }); - - console.log('Download complete, extracting...'); - - // Extract to the base directory - the archive already contains a 'factorio' folder - await new Promise((resolve, reject) => { - const extract = spawn('tar', ['xf', tarballPath, '-C', baseDir]); - - extract.stdout?.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - extract.stderr?.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - extract.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`tar extraction failed with code ${code}`)); - } - }); - }); - - // Clean up the tarball - await fs.unlink(tarballPath); - - console.log('Extraction complete'); - return factorioDir; -}; - -const findModFile = async () => { - const distDir = path.join(__dirname, '..', 'dist'); - const files = await fs.readdir(distDir); - - let modFile; - if (modVersion) { - // Look for a specific version if specified - modFile = files.find(file => file === `subspace_storage_${modVersion}.zip`); - } else { - // Get info.json to find all versions - const infoJson = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'src', 'info.json'))); - - // Find the variant that corresponds to our factorio version - const variant = infoJson.variants.find(v => v.factorio_version === factorioVersion.split('.').slice(0, 2).join('.')); - - if (!variant) { - throw new Error(`No mod variant found for Factorio ${factorioVersion}`); - } - - modFile = files.find(file => file === `subspace_storage_${variant.version}.zip`); - } - - if (!modFile) { - throw new Error('Could not find mod file in dist directory'); - } - - return path.join(distDir, modFile); -}; - -const downloadClusterioLib = async (modDir) => { - console.log('Downloading clusterio_lib from GitHub Actions...'); - - if (!githubToken) { - throw new Error('GITHUB_TOKEN environment variable is required to download artifacts from GitHub Actions'); - } - - // First, get the latest workflow run ID - const getLatestRunId = async () => { - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: '/repos/clusterio/clusterio/actions/runs?status=success&per_page=1', - method: 'GET', - headers: { - 'User-Agent': 'subspace-storage-integration-test', - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json' - } - }; - - const req = https.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - reject(new Error(`Failed to get latest workflow run: ${res.statusCode}, ${data}`)); - return; - } - - try { - const response = JSON.parse(data); - if (!response.workflow_runs || response.workflow_runs.length === 0) { - reject(new Error('No workflow runs found')); - return; - } - - resolve(response.workflow_runs[0].id); - } catch (error) { - reject(error); - } - }); - }); - - req.on('error', reject); - req.end(); - }); - }; - - // Get the artifact ID for clusterio_lib - const getArtifactId = async (runId) => { - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: `/repos/clusterio/clusterio/actions/runs/${runId}/artifacts`, - method: 'GET', - headers: { - 'User-Agent': 'subspace-storage-integration-test', - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json' - } - }; - - const req = https.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - reject(new Error(`Failed to get artifacts: ${res.statusCode}, ${data}`)); - return; - } - - try { - const response = JSON.parse(data); - const artifact = response.artifacts.find(a => a.name === 'clusterio_lib'); - if (!artifact) { - reject(new Error('No clusterio_lib artifact found')); - return; - } - - resolve(artifact.id); - } catch (error) { - reject(error); - } - }); - }); - - req.on('error', reject); - req.end(); - }); - }; - - // Download the artifact - const downloadArtifact = async (artifactId) => { - const artifactZipPath = path.join(__dirname, 'clusterio_lib_artifact.zip'); - - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: `/repos/clusterio/clusterio/actions/artifacts/${artifactId}/zip`, - method: 'GET', - headers: { - 'User-Agent': 'subspace-storage-integration-test', - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json' - } - }; - - const req = https.request(options, (res) => { - if (res.statusCode === 302) { - // Follow redirect - const file = createWriteStream(artifactZipPath); - https.get(res.headers.location, (redirectRes) => { - redirectRes.pipe(file); - file.on('finish', () => { - file.close(); - resolve(artifactZipPath); - }); - }).on('error', (err) => { - fs.unlink(artifactZipPath, () => { }); - reject(err); - }); - } else { - reject(new Error(`Expected redirect, got ${res.statusCode}`)); - } - }); - - req.on('error', reject); - req.end(); - }); - }; - - try { - // Step 1: Get the latest workflow run ID - const runId = await getLatestRunId(); - console.log(`Found latest successful workflow run: ${runId}`); - - // Step 2: Get the artifact ID - const artifactId = await getArtifactId(runId); - console.log(`Found clusterio_lib artifact: ${artifactId}`); - - // Step 3: Download the artifact - const artifactZipPath = await downloadArtifact(artifactId); - console.log(`Downloaded artifact to: ${artifactZipPath}`); - - // Step 4: Extract the artifact - const extractDir = path.join(__dirname, 'clusterio_lib_extract'); - await fs.ensureDir(extractDir); - - await pipeline( - fs.createReadStream(artifactZipPath), - Extract({ path: extractDir }) - ); - - // Add delay to allow extraction to properly complete before reading the directory - await new Promise(r => setTimeout(r, 500)); - - // Step 5: Extract all files starting with clusterio_lib_ (older versions just won't be loaded by factorio) - const files = await fs.readdir(extractDir); - const modFiles = files.filter(file => file.startsWith('clusterio_lib_') && file.endsWith('.zip')); - console.log("Mods in artifact:", modFiles); - const validModFiles = modFiles.filter(file => checkModVersionAgainstFactorioVersion( - file - .replace('clusterio_lib_', '') - .replace('.zip', ''), - factorioVersion - )); - - if (validModFiles.length === 0) { - throw new Error('Could not find clusterio_lib mod file in downloaded artifact'); - } - - // Copy the mod file to the mods directory - for (const modFile of validModFiles) { - await fs.copy( - path.join(extractDir, modFile), - path.join(modDir, modFile) - ); - } - - // Clean up - await fs.remove(artifactZipPath); - await fs.remove(extractDir); - - console.log(`Successfully installed clusterio_lib mod:`, validModFiles); - } catch (error) { - throw new Error(`Failed to download clusterio_lib: ${error.message}`); - } -}; - -const setupModDirectory = async (factorioDir) => { - const modDir = path.join(factorioDir, 'mods'); - await fs.ensureDir(modDir); - - // Find the mod file - const modFile = await findModFile(); - console.log(`Using mod file: ${modFile}`); - - // Copy the mod file to the mods directory - await fs.copy(modFile, path.join(modDir, path.basename(modFile))); - - // Download and install the real clusterio_lib from GitHub Actions - if (!githubToken) { - console.warn('GITHUB_TOKEN not provided, unable to download clusterio_lib from GitHub Actions'); - throw new Error('GITHUB_TOKEN not provided'); - } - - await downloadClusterioLib(modDir); - - console.log(`Mods directory set up at: ${modDir}`); - return modDir; -}; +const { downloadFactorio } = require('./downloadFactorio'); +const { setupModDirectory } = require('./setupModDirectory'); + +/* +Required environment variables: +- FACTORIO_VERSION: The version of Factorio to test with (optional, defaults to 1.1.110) +- MOD_VERSION: The version of the mod to test with (optional) +- GITHUB_TOKEN: The GitHub token to use for downloading clusterio_lib from GitHub Actions +*/ const runFactorio = async (factorioDir) => { console.log('Starting Factorio to test mod loading...'); diff --git a/test/setupModDirectory.js b/test/setupModDirectory.js new file mode 100644 index 0000000..082b5b5 --- /dev/null +++ b/test/setupModDirectory.js @@ -0,0 +1,278 @@ +"use strict"; +const fs = require('fs-extra'); +const path = require('path'); +const { createWriteStream } = require('fs'); +const https = require('https'); +const { pipeline } = require('stream/promises'); +const { Extract } = require('unzipper'); + +// Factorio version to test with +const factorioVersion = process.env.FACTORIO_VERSION || '1.1.110'; +const modVersion = process.env.MOD_VERSION; +// GitHub token for API access (can be passed as environment variable) +const githubToken = process.env.GITHUB_TOKEN; + +const setupModDirectory = async (factorioDir) => { + const modDir = path.join(factorioDir, 'mods'); + await fs.ensureDir(modDir); + + // Find the mod file + const modFile = await findModFile(); + console.log(`Using mod file: ${modFile}`); + + // Copy the mod file to the mods directory + await fs.copy(modFile, path.join(modDir, path.basename(modFile))); + + // Download and install the real clusterio_lib from GitHub Actions + if (!githubToken) { + console.warn('GITHUB_TOKEN not provided, unable to download clusterio_lib from GitHub Actions'); + throw new Error('GITHUB_TOKEN not provided'); + } + + await downloadClusterioLib(modDir); + + console.log(`Mods directory set up at: ${modDir}`); + return modDir; +}; +exports.setupModDirectory = setupModDirectory;const downloadClusterioLib = async (modDir) => { + console.log('Downloading clusterio_lib from GitHub Actions...'); + + if (!githubToken) { + throw new Error('GITHUB_TOKEN environment variable is required to download artifacts from GitHub Actions'); + } + + // First, get the latest workflow run ID + const getLatestRunId = async () => { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/repos/clusterio/clusterio/actions/runs?status=success&per_page=1', + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`Failed to get latest workflow run: ${res.statusCode}, ${data}`)); + return; + } + + try { + const response = JSON.parse(data); + if (!response.workflow_runs || response.workflow_runs.length === 0) { + reject(new Error('No workflow runs found')); + return; + } + + resolve(response.workflow_runs[0].id); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', reject); + req.end(); + }); + }; + + // Get the artifact ID for clusterio_lib + const getArtifactId = async (runId) => { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: `/repos/clusterio/clusterio/actions/runs/${runId}/artifacts`, + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`Failed to get artifacts: ${res.statusCode}, ${data}`)); + return; + } + + try { + const response = JSON.parse(data); + const artifact = response.artifacts.find(a => a.name === 'clusterio_lib'); + if (!artifact) { + reject(new Error('No clusterio_lib artifact found')); + return; + } + + resolve(artifact.id); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', reject); + req.end(); + }); + }; + + // Download the artifact + const downloadArtifact = async (artifactId) => { + const artifactZipPath = path.join(__dirname, 'clusterio_lib_artifact.zip'); + + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: `/repos/clusterio/clusterio/actions/artifacts/${artifactId}/zip`, + method: 'GET', + headers: { + 'User-Agent': 'subspace-storage-integration-test', + 'Authorization': `token ${githubToken}`, + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + if (res.statusCode === 302) { + // Follow redirect + const file = createWriteStream(artifactZipPath); + https.get(res.headers.location, (redirectRes) => { + redirectRes.pipe(file); + file.on('finish', () => { + file.close(); + resolve(artifactZipPath); + }); + }).on('error', (err) => { + fs.unlink(artifactZipPath, () => { }); + reject(err); + }); + } else { + reject(new Error(`Expected redirect, got ${res.statusCode}`)); + } + }); + + req.on('error', reject); + req.end(); + }); + }; + + try { + // Step 1: Get the latest workflow run ID + const runId = await getLatestRunId(); + console.log(`Found latest successful workflow run: ${runId}`); + + // Step 2: Get the artifact ID + const artifactId = await getArtifactId(runId); + console.log(`Found clusterio_lib artifact: ${artifactId}`); + + // Step 3: Download the artifact + const artifactZipPath = await downloadArtifact(artifactId); + console.log(`Downloaded artifact to: ${artifactZipPath}`); + + // Step 4: Extract the artifact + const extractDir = path.join(__dirname, 'clusterio_lib_extract'); + await fs.ensureDir(extractDir); + + await pipeline( + fs.createReadStream(artifactZipPath), + Extract({ path: extractDir }) + ); + + // Add delay to allow extraction to properly complete before reading the directory + await new Promise(r => setTimeout(r, 500)); + + // Step 5: Extract all files starting with clusterio_lib_ (older versions just won't be loaded by factorio) + const files = await fs.readdir(extractDir); + const modFiles = files.filter(file => file.startsWith('clusterio_lib_') && file.endsWith('.zip')); + console.log("Mods in artifact:", modFiles); + const validModFiles = modFiles.filter(file => checkModVersionAgainstFactorioVersion( + file + .replace('clusterio_lib_', '') + .replace('.zip', ''), + factorioVersion + )); + + if (validModFiles.length === 0) { + throw new Error('Could not find clusterio_lib mod file in downloaded artifact'); + } + + // Copy the mod file to the mods directory + for (const modFile of validModFiles) { + await fs.copy( + path.join(extractDir, modFile), + path.join(modDir, modFile) + ); + } + + // Clean up + await fs.remove(artifactZipPath); + await fs.remove(extractDir); + + console.log(`Successfully installed clusterio_lib mod:`, validModFiles); + } catch (error) { + throw new Error(`Failed to download clusterio_lib: ${error.message}`); + } +}; +const findModFile = async () => { + const distDir = path.join(__dirname, '..', 'dist'); + const files = await fs.readdir(distDir); + + let modFile; + if (modVersion) { + // Look for a specific version if specified + modFile = files.find(file => file === `subspace_storage_${modVersion}.zip`); + } else { + // Get info.json to find all versions + const infoJson = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'src', 'info.json'))); + + // Find the variant that corresponds to our factorio version + const variant = infoJson.variants.find(v => v.factorio_version === factorioVersion.split('.').slice(0, 2).join('.')); + + if (!variant) { + throw new Error(`No mod variant found for Factorio ${factorioVersion}`); + } + + modFile = files.find(file => file === `subspace_storage_${variant.version}.zip`); + } + + if (!modFile) { + throw new Error('Could not find mod file in dist directory'); + } + + return path.join(distDir, modFile); +}; +const checkModVersionAgainstFactorioVersion = (modVersion, factorioVersion) => { + const modVersionParts = modVersion.split('.'); + const factorioVersionParts = factorioVersion.split('.'); + + switch (modVersionParts[2]) { + case '20': + return factorioVersionParts[0] === '2'; + case '11': + return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '1'; + case ('10', '18'): + return factorioVersionParts[0] === '1' && factorioVersionParts[1] === '0'; + case '17': + return factorioVersionParts[0] === '0' && factorioVersionParts[1] === '17'; + default: + return false; + } +}; From f6b99923c140bf67db2ddbd72f9baf6f1953a523 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 15 Mar 2025 01:30:40 +0100 Subject: [PATCH 09/11] Add testing of lua code --- package.json | 3 +- test/README.md | 69 +++++---- test/lua_commands.test.js | 307 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+), 29 deletions(-) create mode 100644 test/lua_commands.test.js diff --git a/package.json b/package.json index 7dd80ab..0494a3a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "build": "node build.js", - "test": "node test/integration.test.js" + "test": "node test/integration.test.js && node test/lua_commands.test.js" }, "engines": { "node": ">=10" @@ -18,6 +18,7 @@ "yargs": "^17.7.2" }, "devDependencies": { + "rcon-client": "^4.2.5", "tar": "^6.2.0" } } diff --git a/test/README.md b/test/README.md index 284b600..9c7bfab 100644 --- a/test/README.md +++ b/test/README.md @@ -1,49 +1,62 @@ -# Integration Tests for Subspace Storage +# Subspace Storage Integration Tests -This directory contains integration tests for the Subspace Storage mod. +This directory contains integration tests for the Subspace Storage mod. These tests verify that the mod loads correctly and that its Lua commands interact with the game as expected. -## How it works +## Test Files -The integration tests: +- `integration.test.js`: Basic test that verifies the mod loads correctly +- `lua_commands_test.js`: Tests that verify the Lua commands interact with the game correctly -1. Download a headless version of Factorio based on the version specified -2. Find the appropriate mod zip file in the `dist/` directory for the Factorio version -3. Create a dummy `clusterio_lib` mod to satisfy dependencies -4. Start Factorio with the mod and check if it loads without crashing +## Running the Tests -## Running tests locally +### Environment Variables -First, build the mod: +The tests use the following environment variables: -```bash -npm run build -``` +- `FACTORIO_VERSION`: The version of Factorio to test with (optional, defaults to 1.1.110) +- `MOD_VERSION`: The version of the mod to test with (optional) +- `GITHUB_TOKEN`: The GitHub token to use for downloading clusterio_lib from GitHub Actions (required for CI) -Then run the integration tests: +### Running the Basic Integration Test ```bash -npm test +node integration.test.js ``` -You can specify a specific Factorio version to test with: +This test verifies that the mod loads correctly without any errors. + +### Running the Lua Commands Test ```bash -FACTORIO_VERSION=2.0.39 npm test +node lua_commands_test.js ``` -You can also specify a specific mod version to test with: +This test verifies that the Lua commands interact with the game correctly. -```bash -MOD_VERSION=2.1.20 npm test -``` +## How the Tests Work + +The tests work by: + +1. Downloading a headless version of Factorio +2. Setting up a mod directory with the Subspace Storage mod and its dependencies +3. Creating a test save file +4. Running Factorio with Lua scripts that test specific functionality +5. Analyzing the output to determine if the tests passed or failed + +## Adding New Tests + +To add a new test: + +1. Create a new Lua script in the `createTestScripts` function in `lua_commands_test.js` +2. The script should use `game.print("SUCCESS: ...")` to indicate success and `game.print("ERROR: ...")` to indicate failure +3. The test runner will count the number of successes and errors to determine if the test passed -## Test Matrix in CI +## Troubleshooting -In the GitHub Actions workflow, we run a test matrix with the following Factorio versions: +If the tests fail, check the output for error messages. Common issues include: -- 0.17.79 -- 1.0.0 -- 1.1.110 -- 2.0.39 +- Missing dependencies +- Incompatible Factorio version +- Errors in the Lua scripts -These match the respective Factorio versions that the mod supports (0.17, 1.0, 1.1, 2.0). +If you're having trouble with the tests, try running Factorio manually with the mod installed to see if there are any issues. diff --git a/test/lua_commands.test.js b/test/lua_commands.test.js new file mode 100644 index 0000000..dd86ea4 --- /dev/null +++ b/test/lua_commands.test.js @@ -0,0 +1,307 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require('fs-extra'); +const path = require('path'); +const { spawn } = require('child_process'); +const net = require('net'); +const crypto = require('crypto'); +const Rcon = require('rcon-client').Rcon; +const { downloadFactorio } = require('./downloadFactorio'); +const { setupModDirectory } = require('./setupModDirectory'); + +/* +Required environment variables: +- FACTORIO_VERSION: The version of Factorio to test with (optional, defaults to 1.1.110) +- MOD_VERSION: The version of the mod to test with (optional) +- GITHUB_TOKEN: The GitHub token to use for downloading clusterio_lib from GitHub Actions +*/ + +const FACTORIO_VERSION = process.env.FACTORIO_VERSION || "1.1.110"; + +// Create a temporary directory for test scripts +const createTestScripts = async (tempDir) => { + const tests = { + test_inventory_combinator: [` + local surface = game.get_surface(1) + local position = {x = 0, y = 0} + local combinator = surface.create_entity{ + name = "subspace-resource-combinator", + position = position, + force = game.forces.player, + raise_built = true + } + + if not combinator then + rcon.print("ERROR: Failed to create inventory combinator") + return + else + global._test_combinator = combinator + end`, + `-- Set a simulated inventory + local itemsJson = '[["iron-plate",100,"normal"],["copper-plate",50,"normal"]]' + UpdateInvData(itemsJson, true) + `, + `-- Verify the combinator state + local combinator = global._test_combinator + local control = combinator.get_control_behavior() + + local success = true + local function verify_signal(index, item_name, expected_count) + if ${FACTORIO_VERSION.split(".")[0]} < 2 then + local params = control.parameters + local param = params[index] + if not param or param.signal.name ~= item_name or param.count ~= expected_count then + rcon.print(string.format( + "ERROR: Signal mismatch at index %d - Expected %s: %d, Got: %s: %d", + index, + item_name, + expected_count, + param and param.signal.name or "nil", + param and param.count or 0 + )) + success = false + end + else + local section = control.get_section(1) + if not section or section.filters[index].value.name ~= item_name or section.filters[index].min ~= expected_count then + rcon.print(string.format( + "ERROR: Signal mismatch at index %d - Expected %s: %d, Got: %s: %d", + index, + item_name, + expected_count, + section and section.filters[index].value.name or "nil", + section and section.filters[index].min or 0 + )) + success = false + end + end + end + + verify_signal(1, "iron-plate", 100) + verify_signal(2, "copper-plate", 50) + + if success then + rcon.print("SUCCESS: Inventory combinator test passed") + end + `], + }; + + return tests; +}; + +// Function to create a save file +const createSaveFile = async (factorioDir) => { + console.log('Creating initial save file...'); + + const factorioBin = path.join(factorioDir, 'bin', 'x64', 'factorio'); + const savePath = path.join(factorioDir, 'test-save.zip'); + + return new Promise((resolve, reject) => { + const createSave = spawn(factorioBin, [ + '--create', savePath, + '--mod-directory', path.join(factorioDir, 'mods'), + '--map-gen-seed', '12345' // Fixed seed for deterministic tests + ]); + + createSave.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Failed to create save file, exit code: ${code}`)); + return; + } + + console.log('Save file created successfully'); + resolve(savePath); + }); + }); +}; + +// Function to start Factorio server with RCON enabled +const startFactorioServer = async (factorioDir, savePath) => { + console.log('Starting Factorio server with RCON...'); + + const factorioBin = path.join(factorioDir, 'bin', 'x64', 'factorio'); + const rconPassword = crypto.randomBytes(16).toString('hex'); + const rconPort = 27015; + + const factorio = spawn(factorioBin, [ + '--start-server', savePath, + '--mod-directory', path.join(factorioDir, 'mods'), + '--rcon-port', rconPort.toString(), + '--rcon-password', rconPassword + ]); + + let output = ''; + let serverStarted = false; + + factorio.stdout.on('data', (data) => { + const text = data.toString(); + output += text; + process.stdout.write(text); + + if (text.includes('Starting RCON interface')) { + serverStarted = true; + } + }); + + factorio.stderr.on('data', (data) => { + const text = data.toString(); + process.stderr.write(text); + }); + + // Wait for server to start + return new Promise((resolve, reject) => { + const checkStarted = () => { + if (serverStarted) { + resolve({ + process: factorio, + rconPort, + rconPassword, + output: () => output + }); + } else if (factorio.exitCode !== null) { + reject(new Error(`Factorio server exited with code ${factorio.exitCode}`)); + } else { + setTimeout(checkStarted, 100); + } + }; + + checkStarted(); + }); +}; + +// Function to run tests using RCON +const runTestsWithRcon = async (server, tests) => { + console.log('Connecting to RCON...'); + + const rcon = await Rcon.connect({ + host: 'localhost', + port: server.rconPort, + password: server.rconPassword, + timeout: 5000 + }); + + try { + console.log('RCON authenticated, running tests...'); + + const results = { + success: 0, + error: 0, + details: [] + }; + + // Run 2 commands to prime rcon + await rcon.send('/c game.print("Hello, world!")'); + await rcon.send('/c game.print("Hello, world!")'); + + // Run each test + for (const [testName, testScripts] of Object.entries(tests)) { + console.log(`Running test: ${testName}`); + + let combinedResponse = ''; + let hasError = false; + let hasExecutionError = false; + + // Run each script in sequence with delay + for (const script of testScripts) { + // Send the Lua command via RCON + const response = await rcon.send(`/c __subspace_storage__ ${script}`); + combinedResponse += response + '\n'; + + // Check for execution errors after each script + const serverOutput = server.output(); + if (serverOutput.includes('Cannot execute command')) { + hasExecutionError = true; + break; + } + + // Wait ~1 second between scripts + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + // Parse final results + const successCount = (combinedResponse.match(/SUCCESS:/g) || []).length; + const errorCount = hasExecutionError ? 1 : (combinedResponse.match(/ERROR:/g) || []).length; + + console.log(`Test results for ${testName}:`); + console.log(`- Successes: ${successCount}`); + console.log(`- Errors: ${errorCount}`); + + results.success += successCount; + results.error += errorCount; + + results.details.push({ + name: testName, + success: successCount, + error: errorCount, + output: combinedResponse + }); + + if (errorCount > 0 || hasExecutionError) { + console.error(`❌ Test failed: ${testName}`); + console.error(combinedResponse); + if (hasExecutionError) { + console.error('Server reported command execution error'); + console.error(server.output()); + } + } else { + console.log(`✅ Test passed: ${testName}`); + } + } + + return results; + } finally { + await rcon.end(); + } +}; + +const main = async () => { + try { + const factorioDir = await downloadFactorio(); + await setupModDirectory(factorioDir); + + // Always create a fresh save file + const savePath = path.join(factorioDir, 'test-save.zip'); + await createSaveFile(factorioDir); + + // Create test scripts + const tests = await createTestScripts(factorioDir); + + // Start Factorio server with RCON + const server = await startFactorioServer(factorioDir, savePath); + + try { + // Run tests + const results = await runTestsWithRcon(server, tests); + + // Output summary + console.log('\nTest Summary:'); + console.log(`- Total tests: ${Object.keys(tests).length}`); + console.log(`- Total successes: ${results.success}`); + console.log(`- Total errors: ${results.error}`); + + if (results.error > 0) { + console.error('\nSome tests failed. Check the output above for details.'); + process.exit(1); + } else { + console.log('\nAll Lua command tests passed successfully!'); + process.exit(0); + } + } finally { + // Kill the Factorio server + server.process.kill(); + // Clean up the save file + try { + await fs.remove(savePath); + console.log('Test save file cleaned up'); + } catch (err) { + console.warn('Failed to clean up test save file:', err); + } + } + } catch (error) { + console.error('Integration test failed:', error); + process.exit(1); + } +}; + +main(); From 0f5bc3f25c5a13a7d18f728b96e704396d0e39e9 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 15 Mar 2025 01:31:05 +0100 Subject: [PATCH 10/11] Fix bug causing crash when using inventory combinator pre 2.0 --- .github/workflows/node.js.yml | 2 +- src/control.lua | 5 ++++- src/info.json | 8 ++++---- src/prototypes/entities.lua | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 686e2ce..eb89c92 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -69,4 +69,4 @@ jobs: env: FACTORIO_VERSION: ${{ matrix.factorio-version }} GITHUB_TOKEN: ${{ secrets.GH_PAT }} - run: node test/integration.test.js + run: npm test diff --git a/src/control.lua b/src/control.lua index 863127b..2bf1313 100644 --- a/src/control.lua +++ b/src/control.lua @@ -5,7 +5,7 @@ local mod_gui = require("mod-gui") local clusterio_api = require("__clusterio_lib__/api") local lib_compat = require("__clusterio_lib__/compat") -local compat = require("compat") +local compat = require("lib_compat") -- Entities which are not allowed to be placed outside the restriction zone @@ -204,6 +204,9 @@ script.on_event(defines.events.on_robot_built_entity, function(event) OnBuiltEntity(event) end) +script.on_event(defines.events.script_raised_built, function(event) + OnBuiltEntity(event) +end) ---------------------------- --[[Thing killing events]]-- diff --git a/src/info.json b/src/info.json index 00307c1..2a47de1 100644 --- a/src/info.json +++ b/src/info.json @@ -4,22 +4,22 @@ { "version": "2.1.17", "factorio_version": "0.17", - "additional_files": { "compat.lua": ["compat", "factorio_0.17.lua"] } + "additional_files": { "lib_compat.lua": ["compat", "factorio_0.17.lua"] } }, { "version": "2.1.10", "factorio_version": "1.0", - "additional_files": { "compat.lua": ["compat", "factorio_1.0.lua"] } + "additional_files": { "lib_compat.lua": ["compat", "factorio_1.0.lua"] } }, { "version": "2.1.11", "factorio_version": "1.1", - "additional_files": { "compat.lua": ["compat", "factorio_1.1.lua"] } + "additional_files": { "lib_compat.lua": ["compat", "factorio_1.1.lua"] } }, { "version": "2.1.20", "factorio_version": "2.0", - "additional_files": { "compat.lua": ["compat", "factorio_2.0.lua"] } + "additional_files": { "lib_compat.lua": ["compat", "factorio_2.0.lua"] } } ], "title": "Subspace Storage (Alpha)", diff --git a/src/prototypes/entities.lua b/src/prototypes/entities.lua index 33d09d3..85220e2 100644 --- a/src/prototypes/entities.lua +++ b/src/prototypes/entities.lua @@ -1,4 +1,4 @@ -local compat = require("compat") +local compat = require("lib_compat") local icons = require("entity_icons") local pictures = require("entity_pictures") From e31f7c114396567b9a96c2889c1b5057f11e2839 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 15 Mar 2025 10:23:51 +0100 Subject: [PATCH 11/11] Update test to run in <= 1.0.0 --- test/lua_commands.test.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/lua_commands.test.js b/test/lua_commands.test.js index dd86ea4..e5a0bc2 100644 --- a/test/lua_commands.test.js +++ b/test/lua_commands.test.js @@ -38,19 +38,18 @@ const createTestScripts = async (tempDir) => { else global._test_combinator = combinator end`, - `-- Set a simulated inventory + `--[[ Set a simulated inventory ]] local itemsJson = '[["iron-plate",100,"normal"],["copper-plate",50,"normal"]]' UpdateInvData(itemsJson, true) `, - `-- Verify the combinator state + `--[[ Verify the combinator state ]] local combinator = global._test_combinator local control = combinator.get_control_behavior() local success = true local function verify_signal(index, item_name, expected_count) if ${FACTORIO_VERSION.split(".")[0]} < 2 then - local params = control.parameters - local param = params[index] + local param = control.get_signal(index) if not param or param.signal.name ~= item_name or param.count ~= expected_count then rcon.print(string.format( "ERROR: Signal mismatch at index %d - Expected %s: %d, Got: %s: %d", @@ -205,7 +204,7 @@ const runTestsWithRcon = async (server, tests) => { // Run each script in sequence with delay for (const script of testScripts) { // Send the Lua command via RCON - const response = await rcon.send(`/c __subspace_storage__ ${script}`); + const response = await rcon.send(`/c __subspace_storage__ ${script.replace(/\n/g, ' ')}`); combinedResponse += response + '\n'; // Check for execution errors after each script