diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c09290b..d40192b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,12 @@ env: jobs: test: - runs-on: ubuntu-latest + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Checkout code @@ -30,6 +35,12 @@ jobs: - name: Install dependencies run: npm ci + - name: Install CodeCarbon + run: python -m pip install --upgrade pip codecarbon + + - name: Integration smoke checks + run: npm run test:smoke + - name: Run Python tests run: npm run test:python diff --git a/.gitignore b/.gitignore index bcb7a13..a89fe45 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ bundled/libs/ **/.pytest_cache **/.vs -out-tests \ No newline at end of file +out-tests +# Codecarbon emissions data +emissions.csv diff --git a/README.md b/README.md index 1f2b247..579ac21 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,16 @@ If you want keyboard shortcuts, add these to your `keybindings.json`: ## Requirements -The extension uses `codecarbon` to measure the carbon emissions. This package connects to your hardware via specific APIs to get to know the power usage of your CPU/GPU/RAM. These APIs depend on the brand and OS. See https://mlco2.github.io/codecarbon/methodology.html#power-usage for the needed tools for your specific setup. +The extension uses `codecarbon` to measure the carbon emissions. This package connects to your hardware via specific APIs to get to know the power usage of your CPU/GPU/RAM. These APIs depend on the brand and OS. See https://docs.codecarbon.io/latest/introduction/methodology/#power-usage for the needed tools for your specific setup. > Note: if you do not install the requirements, codecarbon will track in fallback mode. +## Cross-platform caveats + +- The extension behavior is designed to be consistent on Linux, macOS, and Windows (same start/stop commands, status bar state model, and logs). +- Hardware-level power collection accuracy depends on OS-specific CodeCarbon dependencies and vendor tooling. Review the official CodeCarbon power usage requirements in the link above. +- When those dependencies are unavailable, CodeCarbon may use fallback estimation mode; the extension still runs, but metrics fidelity can differ by platform. + ## Extension Settings This extension contributes the following settings: diff --git a/package.json b/package.json index e6a6f75..caf87dd 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,8 @@ "package": "vsce package", "test:python": "python3 -m unittest discover -s tests/python -p \"test_*.py\"", "test:ts": "tsc -p tsconfig.tests.json && node --test out-tests/tests/ts/*.test.js", - "test": "npm run test:python && npm run test:ts" + "test:smoke": "node --test tests/integration/*.test.mjs", + "test": "npm run test:smoke && npm run test:python && npm run test:ts" }, "devDependencies": { "@types/node": "^25.5.0", diff --git a/tests/integration/smoke.test.mjs b/tests/integration/smoke.test.mjs new file mode 100644 index 0000000..8b944c7 --- /dev/null +++ b/tests/integration/smoke.test.mjs @@ -0,0 +1,112 @@ +import { execFile, spawn } from 'node:child_process'; +import assert from 'node:assert/strict'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import process from 'node:process'; +import test from 'node:test'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const workspaceRoot = path.resolve(__dirname, '../..'); +const trackerScript = path.resolve(workspaceRoot, 'src/scripts/tracker.py'); +const pythonCmd = process.env.PYTHON_CMD || 'python'; + +function execPython(args) { + return new Promise((resolve) => { + execFile(pythonCmd, args, (error, stdout, stderr) => { + resolve({ + ok: !error, + stdout: stdout?.toString() ?? '', + stderr: stderr?.toString() ?? '', + code: error?.code ?? 0, + }); + }); + }); +} + +async function checkInterpreterDiscovery() { + const version = await execPython(['--version']); + assert.equal( + version.ok, + true, + `Interpreter discovery failed for "${pythonCmd}"\n${version.stdout}\n${version.stderr}`, + ); + console.log(`Interpreter discovery OK: ${(version.stdout || version.stderr).trim()}`); + + const pip = await execPython(['-m', 'pip', '--version']); + assert.equal(pip.ok, true, `pip is unavailable for selected interpreter\n${pip.stdout}\n${pip.stderr}`); + console.log(`pip check OK: ${pip.stdout.trim()}`); +} + +async function checkPackagePresenceProbe() { + const show = await execPython(['-m', 'pip', 'show', 'codecarbon']); + if (show.ok) { + console.log('Package presence check OK: codecarbon is installed.'); + return; + } + + if (show.code === 1) { + console.log('Package presence check OK: codecarbon not installed (probe behavior validated).'); + return; + } + + assert.fail(`Package presence probe failed unexpectedly\n${show.stdout}\n${show.stderr}`); +} + +async function checkTrackerLifecycle() { + await new Promise((resolve, reject) => { + const child = spawn(pythonCmd, ['-u', trackerScript, 'start'], { + cwd: workspaceRoot, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const timeoutMs = 20000; + const timeout = setTimeout(() => { + child.kill(); + reject(new Error(`Tracker did not start within ${timeoutMs}ms.`)); + }, timeoutMs); + + let started = false; + let output = ''; + + const onData = (chunk) => { + const text = chunk.toString(); + output += text; + if (!started && (text.includes('Starting the tracker...') || text.includes('Tracker started.'))) { + started = true; + setTimeout(() => child.kill(), 1500); + } + }; + + child.stdout?.on('data', onData); + child.stderr?.on('data', onData); + + child.on('close', (code) => { + clearTimeout(timeout); + if (!started) { + reject(new Error(`Tracker lifecycle failed before start (exit code: ${code}).\n${output}`)); + return; + } + console.log(`Tracker lifecycle OK (exit code: ${code ?? 'unknown'}).`); + resolve(); + }); + + child.on('error', (error) => { + clearTimeout(timeout); + reject(error); + }); + }); +} + +test('integration smoke: interpreter discovery', async () => { + console.log(`Running integration smoke test with interpreter: ${pythonCmd}`); + await checkInterpreterDiscovery(); +}); + +test('integration smoke: package presence probe', async () => { + await checkPackagePresenceProbe(); +}); + +test('integration smoke: tracker lifecycle spawn/stop', async () => { + await checkTrackerLifecycle(); + console.log('All integration smoke checks passed.'); +});