diff --git a/CHANGELOG.md b/CHANGELOG.md index 9575994d..8c81e4c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [3.5.1] - 2025-11-20 + +- Fixed CLI hash comparison on Windows +- Improved error messages on failure to run CLI + ## [3.5.0] - 2025-10-09 - Improved exercise submission packaging to avoid overly large archives diff --git a/config.js b/config.js index a8f9f375..a2e0046e 100644 --- a/config.js +++ b/config.js @@ -3,7 +3,7 @@ const path = require("path"); -const TMC_LANGS_RUST_VERSION = "0.39.1"; +const TMC_LANGS_RUST_VERSION = "0.39.4"; const mockTmcLocalMooc = { __TMC_BACKEND_URL__: JSON.stringify("http://localhost:4001"), diff --git a/package-lock.json b/package-lock.json index e5c8dac0..7533effc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "test-my-code", - "version": "3.5.0", + "version": "3.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "test-my-code", - "version": "3.5.0", + "version": "3.5.1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -215,6 +215,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2320,6 +2321,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2391,6 +2393,7 @@ "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2465,6 +2468,7 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -2961,6 +2965,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3020,6 +3025,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3366,6 +3372,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -3460,6 +3467,7 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -4106,6 +4114,7 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4167,6 +4176,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6995,6 +7005,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7468,6 +7479,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8220,6 +8232,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8435,6 +8448,7 @@ "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -8484,6 +8498,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/package.json b/package.json index 14ffd76e..f642e06a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "test-my-code", "displayName": "TestMyCode", - "version": "3.5.0", + "version": "3.5.1", "description": "TestMyCode extension for Visual Studio Code", "categories": [ "Education", diff --git a/src/actions/user.ts b/src/actions/user.ts index a0e0de26..be121f69 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -110,8 +110,13 @@ export async function testExercise( if (!course.perhapsExamMode) { const executablePath = getActiveEditorExecutablePath(actionContext); - const [testRunner, testInterrupt] = tmc.val.runTests(exercise.uri.fsPath, executablePath); - const [validationRunner, validationInterrupt] = tmc.val.runCheckstyle(exercise.uri.fsPath); + const { process: testRunner, interrupt: testInterrupt } = tmc.val.runTests( + exercise.uri.fsPath, + executablePath, + ); + const { process: validationRunner, interrupt: validationInterrupt } = tmc.val.runCheckstyle( + exercise.uri.fsPath, + ); testInterrupts.set(testRunId, [testInterrupt, validationInterrupt]); const exerciseName = exercise.exerciseSlug; diff --git a/src/api/tmc.ts b/src/api/tmc.ts index 6f9d4be5..996131f6 100644 --- a/src/api/tmc.ts +++ b/src/api/tmc.ts @@ -12,9 +12,11 @@ import { ConnectionError, EmptyLangsResponseError, ForbiddenError, + InitializationError, InvalidTokenError, ObsoleteClientError, RuntimeError, + SpawnError, } from "../errors"; import { CliOutput, @@ -261,12 +263,15 @@ export default class TMC { exercisePath: string, pythonExecutablePath?: string, progressCallback?: (progressPct: number, message?: string) => void, - ): [Promise>, () => void] { + ): { + process: Promise>; + interrupt: () => void; + } { const env: { [key: string]: string } = {}; if (pythonExecutablePath) { env.TMC_LANGS_PYTHON_EXEC = pythonExecutablePath; } - const { interrupt, result } = this._spawnLangsProcess({ + const process = this._spawnLangsProcess({ args: ["run-tests", "--exercise-path", exercisePath], env, onStdout: (data) => @@ -274,32 +279,43 @@ export default class TMC { onStderr: (data) => Logger.info("Rust Langs", data), processTimeout: CLI_PROCESS_TIMEOUT, }); + if (process.err) { + return { process: Promise.resolve(process), interrupt: (): void => {} }; + } + const { interrupt, result } = process.val; const postResult = result.then((res) => res .andThen((x) => this._checkLangsResponse(x, "test-result")) .map((x) => x.data["output-data"]), ); - return [postResult, interrupt]; + return { process: postResult, interrupt }; } public runCheckstyle( exercisePath: string, progressCallback?: (progressPct: number, message?: string) => void, - ): [Promise>, () => void] { - const { interrupt, result } = this._spawnLangsProcess({ + ): { + process: Promise>; + interrupt: () => void; + } { + const process = this._spawnLangsProcess({ args: ["checkstyle", "--locale", "en", "--exercise-path", exercisePath], onStdout: (data) => progressCallback?.(100 * data["percent-done"], data.message ?? undefined), onStderr: (data) => Logger.info("Rust Langs", data), processTimeout: CLI_PROCESS_TIMEOUT, }); + if (process.err) { + return { process: Promise.resolve(process), interrupt: (): void => {} }; + } + const { interrupt, result } = process.val; const checkstyleResult = result.then((res) => res .andThen((x) => this._checkLangsResponse(x, "validation")) .map((x) => x.data["output-data"]), ); - return [checkstyleResult, interrupt]; + return { process: checkstyleResult, interrupt }; } // --------------------------------------------------------------------------------------------- @@ -946,7 +962,11 @@ export default class TMC { } } - const res = await this._spawnLangsProcess(langsArgs).result; + const process = this._spawnLangsProcess(langsArgs); + if (process.err) { + return process; + } + const res = await process.val.result; return res .andThen((x) => this._checkLangsResponse(x, outputDataKind)) .andThen((x) => { @@ -1020,7 +1040,9 @@ export default class TMC { * * @returns Rust process runner. */ - private _spawnLangsProcess(commandArgs: LangsProcessArgs): LangsProcessRunner { + private _spawnLangsProcess( + commandArgs: LangsProcessArgs, + ): Result { const { args, env, obfuscate, onStderr, onStdout, stdin, processTimeout } = commandArgs; let theResult: OutputData | undefined; @@ -1044,16 +1066,21 @@ export default class TMC { let active = true; let interrupted = false; - const cprocess = cp.spawn(this.cliPath, args, { - env: { - ...process.env, - ...env, - RUST_LOG: "debug,rustls=warn,reqwest=warn", - TMC_LANGS_TMC_ROOT_URL: tmcBackendUrl, - TMC_LANGS_MOOC_ROOT_URL: moocBackendUrl, - TMC_LANGS_CONFIG_DIR: tmcLangsConfigDir, - }, - }); + let cprocess; + try { + cprocess = cp.spawn(this.cliPath, args, { + env: { + ...process.env, + ...env, + RUST_LOG: "debug,rustls=warn,reqwest=warn", + TMC_LANGS_TMC_ROOT_URL: tmcBackendUrl, + TMC_LANGS_MOOC_ROOT_URL: moocBackendUrl, + TMC_LANGS_CONFIG_DIR: tmcLangsConfigDir, + }, + }); + } catch (error) { + return Err(new SpawnError(error, "Failed to run tmc-langs-cli")); + } if (stdin) { cprocess.stdin.write(stdin + "\n"); } @@ -1190,7 +1217,8 @@ ${error.message}`; kill(cprocess.pid as number); } }; - return { interrupt, result }; + const res = { interrupt, result }; + return Ok(res); } } diff --git a/src/errors.ts b/src/errors.ts index 9c8ff1c8..e50bd608 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -66,3 +66,7 @@ export class FileSystemError extends BaseError { export class ExerciseMigrationError extends BaseError { public readonly name = "Exercise Migration Error"; } + +export class SpawnError extends BaseError { + public readonly name = "Langs Spawn Error"; +} diff --git a/src/extension.ts b/src/extension.ts index a77884a3..4527a60b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,7 +13,12 @@ import { } from "./config/constants"; import Settings from "./config/settings"; import { UserData } from "./config/userdata"; -import { EmptyLangsResponseError, HaltForReloadError, InitializationError } from "./errors"; +import { + EmptyLangsResponseError, + HaltForReloadError, + InitializationError, + SpawnError, +} from "./errors"; import * as init from "./init"; import { randomPanelId, TmcPanel } from "./panels/TmcPanel"; import Storage from "./storage"; @@ -33,10 +38,11 @@ function initializationError(dialog: Dialog, step: string, error: Error, cliFold error, "If this issue is not resolved, the extension may not function properly.", ); - if (error instanceof EmptyLangsResponseError) { - Logger.error( - "The above error may have been caused by an interfering antivirus program. " + - "Please add an exception for the following folder:", + if (error instanceof EmptyLangsResponseError || error instanceof SpawnError) { + Logger.errorWithDialog( + dialog, + "This error may have been caused by an interfering antivirus program. " + + "Please try adding an exception for the following folder:", cliFolder, ); } diff --git a/src/init/ensureLangsUpdated.ts b/src/init/ensureLangsUpdated.ts index 5b724ab8..fb92aa46 100644 --- a/src/init/ensureLangsUpdated.ts +++ b/src/init/ensureLangsUpdated.ts @@ -46,11 +46,13 @@ async function ensureLangsUpdated( const cliHash = new Sha256(); const cliData = fs.readFileSync(cliPath); cliHash.update(cliData); - const cliDigest = Buffer.from(cliHash.digestSync()).toString("hex"); + // windows returns the calculated hash in uppercase for some reason... + const cliDigest = Buffer.from(cliHash.digestSync()).toString("hex").toLowerCase(); - const hashData = fs.readFileSync(shaPath, "utf-8").split(" ")[0]; + const hashData = fs.readFileSync(shaPath, "utf-8").split(" ")[0].trim(); if (cliDigest !== hashData) { Logger.error("Mismatch between CLI and checksum, trying redownload"); + Logger.debug(`CLI "${cliDigest}", hash "${hashData}"`); deleteSync(cliFolder, { force: true }); const result = await downloadLangs( cliFolder, diff --git a/src/test-integration/tmc_langs_cli.spec.ts b/src/test-integration/tmc_langs_cli.spec.ts index f47f3906..11dea113 100644 --- a/src/test-integration/tmc_langs_cli.spec.ts +++ b/src/test-integration/tmc_langs_cli.spec.ts @@ -209,7 +209,7 @@ suite("tmc langs cli spec", function () { }); test("should be able to run tests for exercise", async function () { - const result = await unwrapResult(tmc.runTests(exercisePath)[0]); + const result = await unwrapResult(tmc.runTests(exercisePath).process); expect(result.status).to.be.equal("PASSED"); }); @@ -319,7 +319,7 @@ suite("tmc langs cli spec", function () { }); test("should encounter an error when attempting to run tests for it", async function () { - const result = await tmc.runTests(missingExercisePath)[0]; + const result = await tmc.runTests(missingExercisePath).process; expect(result.val).to.be.instanceOf(RuntimeError); }); @@ -463,7 +463,7 @@ suite("tmc langs cli spec", function () { }); test("should be able to run tests for exercise", async function () { - const result = await unwrapResult(tmc.runTests(exercisePath)[0]); + const result = await unwrapResult(tmc.runTests(exercisePath).process); expect(result.status).to.be.equal("PASSED"); }); diff --git a/webview-ui/src/panels/Welcome.svelte b/webview-ui/src/panels/Welcome.svelte index 79c0d85a..4e210742 100644 --- a/webview-ui/src/panels/Welcome.svelte +++ b/webview-ui/src/panels/Welcome.svelte @@ -72,6 +72,17 @@
+

3.5.1 - 2025-11-20

+

Fixed CLI hash comparison on Windows

+

+ SHA256 hashes calculated on Windows were returned in uppercase, causing spurious + hash comparison failures. This has been fixed. +

+

Improved error messages on failure to run CLI

+

+ If the CLI fails to execute properly for some reason, the error messaging should now + make it more clear to the user how to attempt resolving the issue. +

3.5.0 - 2025-10-09

Improved exercise submission packaging to avoid overly large archives