Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
19 changes: 17 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
9 changes: 7 additions & 2 deletions src/actions/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
66 changes: 47 additions & 19 deletions src/api/tmc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
ConnectionError,
EmptyLangsResponseError,
ForbiddenError,
InitializationError,
InvalidTokenError,
ObsoleteClientError,
RuntimeError,
SpawnError,
} from "../errors";
import {
CliOutput,
Expand Down Expand Up @@ -261,45 +263,59 @@ export default class TMC {
exercisePath: string,
pythonExecutablePath?: string,
progressCallback?: (progressPct: number, message?: string) => void,
): [Promise<Result<RunResult, BaseError>>, () => void] {
): {
process: Promise<Result<RunResult, BaseError | InitializationError>>;
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) =>
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 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<Result<StyleValidationResult | null, BaseError>>, () => void] {
const { interrupt, result } = this._spawnLangsProcess({
): {
process: Promise<Result<StyleValidationResult | null, BaseError>>;
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 };
}

// ---------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -1020,7 +1040,9 @@ export default class TMC {
*
* @returns Rust process runner.
*/
private _spawnLangsProcess(commandArgs: LangsProcessArgs): LangsProcessRunner {
private _spawnLangsProcess(
commandArgs: LangsProcessArgs,
): Result<LangsProcessRunner, InitializationError | SpawnError> {
const { args, env, obfuscate, onStderr, onStdout, stdin, processTimeout } = commandArgs;

let theResult: OutputData | undefined;
Expand All @@ -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");
}
Expand Down Expand Up @@ -1190,7 +1217,8 @@ ${error.message}`;
kill(cprocess.pid as number);
}
};
return { interrupt, result };
const res = { interrupt, result };
return Ok(res);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
16 changes: 11 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/init/ensureLangsUpdated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading