From 04813f4d54833f310029959411ca29597fedb236 Mon Sep 17 00:00:00 2001 From: Daniel Nichols Date: Wed, 4 Dec 2024 11:50:34 -0500 Subject: [PATCH] Add feature to open error output file of active job (#172) * add utility function for opening text files in the editor * refactor to use openTextFile utility * add show error function - adds command and button to items in job dashboard - adds functionality to show job error files - refactors fetching of runtime metadata from jobs with scontrol - adds testing for job error files * correct necessary fields from squeue - stdout is no longer needed * c8 coverage update * add more guards on metadata update * update testing to include errorfile strings * change button order and icons --- package.json | 18 +++- src/fileutilities.ts | 21 ++++ src/jobs.ts | 37 +++---- src/jobscripts.ts | 2 +- src/scheduler.ts | 147 ++++++++++++++++++++------- src/test/example-workspace/job1.err | 0 src/test/slurm-wrapper.py | 21 ++-- src/test/suite/fileutilities.test.ts | 11 ++ src/test/suite/jobs.test.ts | 95 +++++++++++++++-- src/test/suite/scheduler.test.ts | 62 ++++++++++- 10 files changed, 333 insertions(+), 81 deletions(-) create mode 100644 src/test/example-workspace/job1.err diff --git a/package.json b/package.json index 8d505cd..8e51189 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,11 @@ "title": "Show Job Output", "icon": "$(output)" }, + { + "command": "job-dashboard.show-error", + "title": "Show Job Error", + "icon": "$(bracket-error)" + }, { "command": "job-dashboard.show-source", "title": "Show Job Source", @@ -134,22 +139,27 @@ { "command": "job-dashboard.cancel", "when": "view == job-dashboard && viewItem == jobItem", - "group": "inline" + "group": "inline@2" }, { "command": "job-dashboard.cancel-and-resubmit", "when": "view == job-dashboard && viewItem == jobItem", - "group": "inline" + "group": "inline@1" }, { "command": "job-dashboard.show-output", "when": "view == job-dashboard && viewItem == jobItem", - "group": "inline" + "group": "inline@3" + }, + { + "command": "job-dashboard.show-error", + "when": "view == job-dashboard && viewItem == jobItem", + "group": "inline@4" }, { "command": "job-dashboard.show-source", "when": "view == job-dashboard && viewItem == jobItem", - "group": "inline" + "group": "inline@5" }, { "command": "submit-dashboard.submit", diff --git a/src/fileutilities.ts b/src/fileutilities.ts index 87cc023..5ada116 100644 --- a/src/fileutilities.ts +++ b/src/fileutilities.ts @@ -1,5 +1,26 @@ import * as vscode from 'vscode'; +/** + * Opens a text file in the editor. + * + * @param fpath - The relative path to the file to be opened. + * + * This function resolves the given file path relative to the workspace, + * opens the text document, and then shows it in the editor. If there is + * an error during this process, an error message is displayed. + */ +export function openTextFileInEditor(fpath: string): void { + const absPath = resolvePathRelativeToWorkspace(fpath); + vscode.workspace.openTextDocument(absPath).then( + doc => { + vscode.window.showTextDocument(doc); + }, + error => { + vscode.window.showErrorMessage(`Error opening file ${fpath}: ${error}`); + } + ); +} + /** * Resolves the path relative to the workspace. * If the path is not an absolute path or a file URI, it is resolved relative to the workspace root. diff --git a/src/jobs.ts b/src/jobs.ts index af2dd9e..a871c48 100644 --- a/src/jobs.ts +++ b/src/jobs.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { Job, Scheduler, sortJobs } from './scheduler'; -import { resolvePathRelativeToWorkspace } from './fileutilities'; +import { resolvePathRelativeToWorkspace, openTextFileInEditor } from './fileutilities'; /** * Represents an information item in the tree view. These are used to display @@ -248,10 +248,12 @@ export class JobQueueProvider implements vscode.TreeDataProvider this.showOutput(jobItem)); + vscode.commands.registerCommand('job-dashboard.show-error', (jobItem: JobItem) => this.showError(jobItem)); vscode.commands.registerCommand('job-dashboard.show-source', (jobItem: JobItem) => this.showSource(jobItem)); this.initAutoRefresh(); vscode.workspace.onDidChangeConfiguration(e => { + /* c8 ignore next 3 */ if (e.affectsConfiguration('slurm-dashboard.job-dashboard.refreshInterval')) { this.initAutoRefresh(); } @@ -324,34 +326,33 @@ export class JobQueueProvider implements vscode.TreeDataProvider { - vscode.window.showTextDocument(doc); - }, - error => { - vscode.window.showErrorMessage(`Failed to open output file ${jobItem.job.outputFile}.\n${error}`); - } - ); + openTextFileInEditor(fpath); } else { vscode.window.showErrorMessage(`Job ${jobItem.job.id} has no associated output file.`); } } + /** + * Open the error output file for this job in a VSCode text window. + * @param jobItem + */ + private showError(jobItem: JobItem): void { + const fpath = this.scheduler.getJobErrorPath(jobItem.job); + if (fpath) { + openTextFileInEditor(fpath); + } else { + vscode.window.showErrorMessage(`Job ${jobItem.job.id} has no associated error file.`); + } + } + /** * Open the JobItem's source batch file in a VSCode text window. * @param jobItem The job item. */ private showSource(jobItem: JobItem): void { if (jobItem.job.batchFile) { - const fpath = resolvePathRelativeToWorkspace(jobItem.job.batchFile); - vscode.workspace.openTextDocument(fpath).then( - doc => { - vscode.window.showTextDocument(doc); - }, - error => { - vscode.window.showErrorMessage(`Failed to open batch file ${jobItem.job.batchFile}.\n${error}`); - } - ); + const fpath = resolvePathRelativeToWorkspace(jobItem.job.batchFile).toString(); + openTextFileInEditor(fpath); } else { vscode.window.showErrorMessage(`Job ${jobItem.job.id} has no associated batch file.`); } diff --git a/src/jobscripts.ts b/src/jobscripts.ts index ef39f35..8afd373 100644 --- a/src/jobscripts.ts +++ b/src/jobscripts.ts @@ -120,7 +120,7 @@ export class JobScriptProvider implements vscode.TreeDataProvider { const jobScriptExts = vscode.workspace .getConfiguration('slurm-dashboard') .get('submit-dashboard.jobScriptExtensions', ['.slurm', '.sbatch', '.job']); - /* c8 ignore next 6 */ + /* c8 ignore next 7 */ if (JSON.stringify(jobScriptExts) !== JSON.stringify(['.slurm', '.sbatch', '.job'])) { vscode.window.showWarningMessage( 'The slurm-dashboard.submit-dashboard.jobScriptExtensions setting has been modified, but ' + diff --git a/src/scheduler.ts b/src/scheduler.ts index 46d6da1..6c1e69f 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -27,6 +27,7 @@ export class Job { public nodeList?: string, public batchFile?: string, public outputFile?: string, + public errorFile?: string, public maxTime?: WallTime, public curTime?: WallTime ) {} @@ -141,6 +142,14 @@ export interface Scheduler { * @returns The output path for the job or undefined if the job does not have an output file. */ getJobOutputPath(job: Job): string | undefined; + + /** + * Retrieve the error path for a job file. Is permitted to simply return + * job.errorFile. Returns undefined if the job does not have an error file. + * @param job The job for which to retrieve the error path. + * @returns The error path for the job or undefined if the job does not have an error file. + */ + getJobErrorPath(job: Job): string | undefined; } /** @@ -193,7 +202,6 @@ export class SlurmScheduler implements Scheduler { new SchedulerDataColumn('NodeList', 255), new SchedulerDataColumn('Partition', 255), new SchedulerDataColumn('QOS', 255), - new SchedulerDataColumn('STDOUT', 255), new SchedulerDataColumn('TimeLimit', 255), new SchedulerDataColumn('TimeUsed', 255), new SchedulerDataColumn('Command', 255), // command last since it can sometimes have spaces in it @@ -343,6 +351,7 @@ export class SlurmScheduler implements Scheduler { results['NodeList'], results['Command'], undefined /* let this be filled in by getJobOutputPath later */, + undefined /* error path is also patched later */, timeLimit, timeUsed ); @@ -352,6 +361,55 @@ export class SlurmScheduler implements Scheduler { return jobs; } + /** + * Retrieves runtime metadata for a given job by executing the `scontrol show job` command. + * + * @param job - The job object containing the job ID. + * @returns A record containing key-value pairs of the job's runtime metadata. + * @throws Will throw an error if scontrol command fails + */ + private getJobRuntimeMetadata(job: Job): Record { + const metadata: Record = {}; + + const command = `scontrol show job ${job.id}`; + const output = execSync(command).toString().trim(); + const lines = output.split('\n'); + for (let line of lines) { + const pairs = line.split(/\s+/); + for (let pair of pairs) { + const [key, value] = pair.split('='); + if (key && value) { + metadata[key.trim()] = value.trim(); + } + } + } + + return metadata; + } + + /** + * Patches the given job with runtime metadata. + * + * @param job - The job object to be patched. + * @param force - Optional. If true, forces the patching even if the job already has output and error files. Defaults to false. + */ + private patchJobWithRuntimeMetadata(job: Job, force: boolean = false): void { + /* early exit if we already have all the fields */ + if (!force && job.outputFile && job.errorFile) { + return; + } + + const metadata = this.getJobRuntimeMetadata(job); + + if (metadata['StdOut']) { + job.outputFile = metadata['StdOut']; + } + + if (metadata['StdErr']) { + job.errorFile = metadata['StdErr']; + } + } + /** * Finds the path for the standard output file of a job. * Returns job.outputFile if it is already defined. @@ -361,44 +419,48 @@ export class SlurmScheduler implements Scheduler { * @returns The resolved stdout path or undefined if the job does not have an output file. */ public getJobOutputPath(job: Job): string | undefined { - /* early exit if it's already defined */ if (job.outputFile) { - return job.outputFile; + return job.outputFile; /* early exit if it's already defined */ } - const command = `scontrol show job ${job.id}`; - try { - const output = execSync(command).toString().trim(); - - /* find line that starts with StdOut */ - const lines = output.split('\n'); - let stdoutLine: string | undefined = undefined; - for (let line of lines) { - if (line.trim().startsWith('StdOut=')) { - stdoutLine = line.trim(); - break; - } + this.patchJobWithRuntimeMetadata(job); + if (!job.outputFile) { + throw new Error('Failed to get job output path.'); } + } catch (error) { + vscode.window.showErrorMessage(`Failed to get job output path for job ${job.id}.`); + } + return job.outputFile; + } - /* if we didn't find a line, return undefined */ - if (!stdoutLine) { - throw new Error(`Failed to find stdout line in output: ${output}`); - } + /** + * Retrieves the error file path for a given job. + * + * This method first checks if the job's error file path is already defined. + * If it is, it returns the path immediately. If not, it attempts to patch + * the job with runtime metadata to retrieve the error file path. + * + * If the error file path is still not defined after patching, an error message + * is displayed to the user and `undefined` is returned. + * + * @param job - The job for which to retrieve the error file path. + * @returns The error file path as a string if available, otherwise `undefined`. + */ + public getJobErrorPath(job: Job): string | undefined { + if (job.errorFile) { + return job.errorFile; /* early exit if it's already defined */ + } - /* extract path from line: StdOut=/path/to/file */ - const parts = stdoutLine.split('='); - if (parts.length < 2) { - throw new Error(`Failed to parse stdout line: ${stdoutLine}`); + try { + this.patchJobWithRuntimeMetadata(job); + if (!job.errorFile) { + throw new Error('Failed to get job error path.'); } - const fpath = parts[1].trim(); - - job.outputFile = fpath; - return fpath; } catch (error) { - vscode.window.showErrorMessage(`Failed to get job output path for job ${job.id}.\nError: ${error}`); - return undefined; + vscode.window.showErrorMessage(`Failed to get job error path for job ${job.id}.`); } + return job.errorFile; } } @@ -408,15 +470,15 @@ export class Debug implements Scheduler { */ // prettier-ignore private jobs: Job[] = [ - new Job("1", "job1", "RUNNING", "debug", '[node1]', "job1.sh", "job1.out", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 12, 43)), - new Job("2", "job2", "RUNNING", "debug", '[node1]', "job2.sh", "job2.out", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 28, 1)), - new Job("3", "job3", "RUNNING", "debug", '[node1]', "job3.sh", "job3.out", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 1, 15)), - new Job("4", "job4", "PENDING", "debug", '[]', "job4.sh", "job4.out", new WallTime(0, 1, 20, 40), new WallTime(0, 0, 0, 0)), - new Job("5", "job5", "PENDING", "debug", '[]', "job5.sh", "job5.out", new WallTime(1, 12, 0, 0), new WallTime(0, 0, 0, 0)), - new Job("6", "job6", "COMPLETED", "debug", '[]', "job6.sh", "job6.out", new WallTime(0, 7, 0, 0), new WallTime(0, 7, 0, 0)), - new Job("7", "job7", "TIMEOUT", "debug", '[]', "job7.sh", "job7.out", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 30, 0)), - new Job("8", "job8", "CANCELLED", "debug", '[]', "job8.sh", "job8.out", new WallTime(0, 23, 59, 59), new WallTime(0, 0, 0, 0)), - new Job("9", "job9", "FAILED", "debug", '[]', "job9.sh", "job9.out", new WallTime(0, 0, 5, 0), new WallTime(0, 0, 0, 0)), + new Job("1", "job1", "RUNNING", "debug", '[node1]', "job1.sh", "job1.out", "job1.err", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 12, 43)), + new Job("2", "job2", "RUNNING", "debug", '[node1]', "job2.sh", "job2.out", "job2.err", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 28, 1)), + new Job("3", "job3", "RUNNING", "debug", '[node1]', "job3.sh", "job3.out", "job3.err", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 1, 15)), + new Job("4", "job4", "PENDING", "debug", '[]', "job4.sh", "job4.out", "job4.err", new WallTime(0, 1, 20, 40), new WallTime(0, 0, 0, 0)), + new Job("5", "job5", "PENDING", "debug", '[]', "job5.sh", "job5.out", "job5.err", new WallTime(1, 12, 0, 0), new WallTime(0, 0, 0, 0)), + new Job("6", "job6", "COMPLETED", "debug", '[]', "job6.sh", "job6.out", "job6.err", new WallTime(0, 7, 0, 0), new WallTime(0, 7, 0, 0)), + new Job("7", "job7", "TIMEOUT", "debug", '[]', "job7.sh", "job7.out", "job7.err", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 30, 0)), + new Job("8", "job8", "CANCELLED", "debug", '[]', "job8.sh", "job8.out", "job8.err", new WallTime(0, 23, 59, 59), new WallTime(0, 0, 0, 0)), + new Job("9", "job9", "FAILED", "debug", '[]', "job9.sh", "job9.out", "job9.err", new WallTime(0, 0, 5, 0), new WallTime(0, 0, 0, 0)), ]; /** @@ -452,6 +514,15 @@ export class Debug implements Scheduler { public getJobOutputPath(job: Job): string | undefined { return job.outputFile; } + + /** + * For the debug scheduler, just returns the job's error file. + * @param job The job for which to retrieve the error path. + * @returns The error path for the job or undefined if the job does not have an error file. + */ + public getJobErrorPath(job: Job): string | undefined { + return job.errorFile; + } } /** diff --git a/src/test/example-workspace/job1.err b/src/test/example-workspace/job1.err new file mode 100644 index 0000000..e69de29 diff --git a/src/test/slurm-wrapper.py b/src/test/slurm-wrapper.py index b175097..5823c48 100755 --- a/src/test/slurm-wrapper.py +++ b/src/test/slurm-wrapper.py @@ -30,7 +30,7 @@ # Job wrapper class class Job: - def __init__(self, id, name, status, queue, nodeList, batchFile, outputFile, maxTime, curTime): + def __init__(self, id, name, status, queue, nodeList, batchFile, outputFile, errorFile, maxTime, curTime): self.id = id self.name = name self.status = status @@ -38,16 +38,17 @@ def __init__(self, id, name, status, queue, nodeList, batchFile, outputFile, max self.nodeList = nodeList self.batchFile = batchFile self.outputFile = outputFile + self.errorFile = errorFile self.maxTime = maxTime self.curTime = curTime def __str__(self): - return f'{self.id} {self.name} {self.status} {self.queue} {self.nodeList} {self.batchFile} {self.outputFile} {self.maxTime} {self.curTime}' + return f'{self.id} {self.name} {self.status} {self.queue} {self.nodeList} {self.batchFile} {self.outputFile} {self.errorFile} {self.maxTime} {self.curTime}' @staticmethod def fromStr(s: str): - id, name, status, queue, nodeList, batchFile, outputFile, maxTime, curTime = s.split() - return Job(id, name, status, queue, nodeList, batchFile, outputFile, maxTime, curTime) + id, name, status, queue, nodeList, batchFile, outputFile, errorFile, maxTime, curTime = s.split() + return Job(id, name, status, queue, nodeList, batchFile, outputFile, errorFile, maxTime, curTime) def write_jobs(job_list): @@ -66,7 +67,8 @@ def sbatch(script): max_id = max([int(job.id) for job in jobs]) job_name = os.path.basename(script).split('.')[0] output = f"{job_name}-{max_id+1}.out" - job = Job(str(max_id+1), job_name, 'PENDING', '[]', 'batch', script, output, '00:00:00', '00:00:00') + error = f"{job_name}-{max_id+1}.err" + job = Job(str(max_id+1), job_name, 'PENDING', '[]', 'batch', script, output, error, '00:00:00', '00:00:00') jobs.append(job) write_jobs(jobs) print(f'Submitted batch job {job.id}') @@ -78,7 +80,7 @@ def squeue(me=False, noheader=False, O=None): jobs = read_jobs() for j in jobs: - fields = [j.id, j.name, j.status, j.queue, j.nodeList, j.queue, j.outputFile, j.maxTime, j.curTime, j.batchFile] + fields = [j.id, j.name, j.status, j.nodeList, j.queue, j.queue, j.maxTime, j.curTime, j.batchFile] print(' '.join(fields)) def scancel(jobid): @@ -99,15 +101,16 @@ def scontrol_show(field, value): print(f'Partition={j.queue}') print(f'Command={j.batchFile}') print(f'StdOut={j.outputFile}') + print(f'StdErr={j.errorFile}') print(f'Timelimit={j.maxTime}') # reset data in file if args.command == 'sreset': jobs = [ - Job('123456', 'job1', 'COMPLETED', 'batch', '[]', 'job1.sbatch', 'job1.out', '1-00:00:00', '01:37:16'), - Job('123457', 'job2', 'RUNNING', 'batch', '[node1]', 'job2.sbatch', 'job2.out', '06:00:00', '00:14:39'), - Job('123458', 'job3', 'PENDING', 'batch', '[]', 'more/job3.job', 'job3.out', '00:15:00', '00:00:00'), + Job('123456', 'job1', 'COMPLETED', 'batch', '[]', 'job1.sbatch', 'job1.out', 'job1.err', '1-00:00:00', '01:37:16'), + Job('123457', 'job2', 'RUNNING', 'batch', '[node1]', 'job2.sbatch', 'job2.out', 'job2.err', '06:00:00', '00:14:39'), + Job('123458', 'job3', 'PENDING', 'batch', '[]', 'more/job3.job', 'job3.out', 'job3.err', '00:15:00', '00:00:00'), ] write_jobs(jobs) elif args.command == 'sbatch': diff --git a/src/test/suite/fileutilities.test.ts b/src/test/suite/fileutilities.test.ts index 83f3875..d922dc0 100644 --- a/src/test/suite/fileutilities.test.ts +++ b/src/test/suite/fileutilities.test.ts @@ -6,6 +6,17 @@ import * as vscode from 'vscode'; import * as fu from '../../fileutilities'; suite('fileutilities.ts tests', () => { + test('openTextFileInEditor', () => { + { + const filePath = 'job1.sbatch'; + assert.doesNotThrow(() => fu.openTextFileInEditor(filePath), 'openTextFileInEditor'); + } + { + const filePathNotExists = 'job1.sbatch.notexists'; + assert.doesNotThrow(() => fu.openTextFileInEditor(filePathNotExists), 'openTextFileInEditor'); + } + }); + test('resolvePathRelativeToWorkspace', () => { { // absolute path diff --git a/src/test/suite/jobs.test.ts b/src/test/suite/jobs.test.ts index 4764520..b17873a 100644 --- a/src/test/suite/jobs.test.ts +++ b/src/test/suite/jobs.test.ts @@ -37,6 +37,7 @@ suite('jobs.ts tests', () => { '[]', 'batch', 'out', + 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ), @@ -61,7 +62,7 @@ suite('jobs.ts tests', () => { { const jobItem = new jobs.JobItem( /* prettier-ignore */ - new Job('2','Test Job', 'RUNNING', 'queue', '[node1]', 'batch', 'out', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 29, 30)) + new Job('2','Test Job', 'RUNNING', 'queue', '[node1]', 'batch', 'out', 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 29, 30)) ); const iconPath = jobItem.getIconPath(); assert.ok(iconPath !== undefined); @@ -110,7 +111,7 @@ suite('jobs.ts tests', () => { { const jobItem = new jobs.JobItem( /* prettier-ignore */ - new Job('2', 'Test Job', 'RUNNING', 'queue', '[node1]', 'batch', 'out', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 29, 30)) + new Job('2', 'Test Job', 'RUNNING', 'queue', '[node1]', 'batch', 'out', 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 29, 30)) ); assert.deepEqual(jobItem.getIconPath(), new vscode.ThemeIcon('play')); } @@ -167,6 +168,7 @@ suite('jobs.ts tests', () => { '[]', 'batch', 'out', + 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ), @@ -203,6 +205,7 @@ suite('jobs.ts tests', () => { '[node1]', 'batch', 'out', + 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ); @@ -222,6 +225,7 @@ suite('jobs.ts tests', () => { '[]', 'batch', 'out', + 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ); @@ -241,6 +245,7 @@ suite('jobs.ts tests', () => { '[node1]', 'batch', 'out', + 'err', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 29, 50) ); @@ -416,16 +421,86 @@ suite('jobs.ts tests', () => { }); test('commands :: job-dashboard.show-output', async function () { - assert.doesNotThrow(async () => { - const jobItem = new jobs.JobItem(new Job('1', 'Test Job', 'RUNNING'), false); - await vscode.commands.executeCommand('job-dashboard.show-output', jobItem); - }); + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', '', 'job1.out', 'job1.err'), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-output', jobItem); + }); + } + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', '', 'job1.out.fake', 'job1.err'), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-output', jobItem); + }); + } + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', '', undefined, 'job1.err'), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-output', jobItem); + }); + } + }); + + test('commands :: job-dashboard.show-error', async function () { + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', '', 'job1.out', 'job1.err'), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-error', jobItem); + }); + } + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', '', 'job1.out', 'job1.err.fake'), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-error', jobItem); + }); + } + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', '', 'job1.out', undefined), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-error', jobItem); + }); + } }); test('commands :: job-dashboard.show-source', async function () { - assert.doesNotThrow(async () => { - const jobItem = new jobs.JobItem(new Job('1', 'Test Job', 'RUNNING'), false); - await vscode.commands.executeCommand('job-dashboard.show-source', jobItem); - }); + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem(new Job('1', 'Test Job', 'RUNNING', '', '', 'job1.sbatch'), false); + await vscode.commands.executeCommand('job-dashboard.show-source', jobItem); + }); + } + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem( + new Job('1', 'Test Job', 'RUNNING', '', '', 'job1.sbatch.fake'), + false + ); + await vscode.commands.executeCommand('job-dashboard.show-source', jobItem); + }); + } + { + assert.doesNotThrow(async () => { + const jobItem = new jobs.JobItem(new Job('1', 'Test Job', 'RUNNING', '', '', undefined), false); + await vscode.commands.executeCommand('job-dashboard.show-source', jobItem); + }); + } }); }); diff --git a/src/test/suite/scheduler.test.ts b/src/test/suite/scheduler.test.ts index dc9f5af..daa23c4 100644 --- a/src/test/suite/scheduler.test.ts +++ b/src/test/suite/scheduler.test.ts @@ -28,6 +28,7 @@ suite('scheduler.ts tests', () => { assert.strictEqual(job.queue, undefined); assert.strictEqual(job.batchFile, undefined); assert.strictEqual(job.outputFile, undefined); + assert.strictEqual(job.errorFile, undefined); assert.strictEqual(job.maxTime, undefined); assert.strictEqual(job.curTime, undefined); } @@ -40,6 +41,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 0, 3600), new WallTime(0, 0, 0, 1800) ); @@ -49,6 +51,7 @@ suite('scheduler.ts tests', () => { assert.strictEqual(job.queue, 'queue'); assert.strictEqual(job.batchFile, 'batchFile'); assert.strictEqual(job.outputFile, 'outputFile'); + assert.strictEqual(job.errorFile, 'errorFile'); assert.strictEqual(job.maxTime?.toSeconds(), 3600); assert.strictEqual(job.curTime?.toSeconds(), 1800); } @@ -68,6 +71,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 0, 3600), new WallTime(0, 0, 0, 1800) ); @@ -82,6 +86,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ); @@ -103,6 +108,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 0, 3600), new WallTime(0, 0, 0, 1800) ); @@ -117,6 +123,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ); @@ -165,6 +172,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 0, 3600), new WallTime(0, 0, 0, 1800) ), @@ -176,6 +184,7 @@ suite('scheduler.ts tests', () => { '[]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 30) ), @@ -187,6 +196,7 @@ suite('scheduler.ts tests', () => { '[]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 0, 3600), new WallTime(0, 0, 0, 0) ), @@ -198,6 +208,7 @@ suite('scheduler.ts tests', () => { '[]', 'batchFile', 'outputFile', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 15, 45) ), @@ -387,6 +398,34 @@ suite('scheduler.ts tests', () => { assert.strictEqual(slurm.getJobOutputPath(queue[0]), 'job1.out'); assert.strictEqual(slurm.getJobOutputPath(queue[1]), 'job2.out'); assert.strictEqual(slurm.getJobOutputPath(queue[2]), 'job3.out'); + + // try again to test caching + assert.strictEqual(slurm.getJobOutputPath(queue[0]), 'job1.out'); + assert.strictEqual(slurm.getJobOutputPath(queue[1]), 'job2.out'); + assert.strictEqual(slurm.getJobOutputPath(queue[2]), 'job3.out'); + }); + + test('Slurm :: getJobErrorPath', async function () { + /* skip if windows */ + if (process.platform === 'win32') { + this.skip(); + } + + assert.doesNotThrow(async () => { + execSync('sreset'); + }); + + const slurm = new scheduler.SlurmScheduler(); + const queue = await slurm.getQueue(); + + assert.strictEqual(slurm.getJobErrorPath(queue[0]), 'job1.err'); + assert.strictEqual(slurm.getJobErrorPath(queue[1]), 'job2.err'); + assert.strictEqual(slurm.getJobErrorPath(queue[2]), 'job3.err'); + + // try again to check caching + assert.strictEqual(slurm.getJobErrorPath(queue[0]), 'job1.err'); + assert.strictEqual(slurm.getJobErrorPath(queue[1]), 'job2.err'); + assert.strictEqual(slurm.getJobErrorPath(queue[2]), 'job3.err'); }); test('Debug :: getQueue', () => { @@ -399,6 +438,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'job1.sh', 'job1.out', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 12, 43) ), @@ -410,6 +450,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'job2.sh', 'job2.out', + 'errorFile', new WallTime(0, 1, 30, 0), new WallTime(0, 1, 28, 1) ), @@ -421,6 +462,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'job3.sh', 'job3.out', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 1, 15) ), @@ -432,6 +474,7 @@ suite('scheduler.ts tests', () => { '[]', 'job4.sh', 'job4.out', + 'errorFile', new WallTime(0, 1, 20, 40), new WallTime(0, 0, 0, 0) ), @@ -443,6 +486,7 @@ suite('scheduler.ts tests', () => { '[]', 'job5.sh', 'job5.out', + 'errorFile', new WallTime(1, 12, 0, 0), new WallTime(0, 0, 0, 0) ), @@ -454,6 +498,7 @@ suite('scheduler.ts tests', () => { '[]', 'job6.sh', 'job6.out', + 'errorFile', new WallTime(0, 7, 0, 0), new WallTime(0, 7, 0, 0) ), @@ -465,6 +510,7 @@ suite('scheduler.ts tests', () => { '[]', 'job7.sh', 'job7.out', + 'errorFile', new WallTime(0, 1, 30, 0), new WallTime(0, 1, 30, 0) ), @@ -476,6 +522,7 @@ suite('scheduler.ts tests', () => { '[]', 'job8.sh', 'job8.out', + 'errorFile', new WallTime(0, 23, 59, 59), new WallTime(0, 0, 0, 0) ), @@ -487,6 +534,7 @@ suite('scheduler.ts tests', () => { '[]', 'job9.sh', 'job9.out', + 'errorFile', new WallTime(0, 0, 5, 0), new WallTime(0, 0, 0, 0) ), @@ -521,6 +569,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'job1.sh', 'job1.out', + 'errorFile', new WallTime(0, 0, 30, 0), new WallTime(0, 0, 12, 43) ) @@ -534,6 +583,7 @@ suite('scheduler.ts tests', () => { '[node1]', 'job2.sh', 'job2.out', + 'errorFile', new WallTime(0, 1, 30, 0), new WallTime(0, 1, 28, 1) ) @@ -551,7 +601,7 @@ suite('scheduler.ts tests', () => { assert.doesNotThrow(() => debug.submitJob('job1.sh')); }); - test('Debug :: getJobOutput', () => { + test('Debug :: getJobOutputPath', () => { let debug = new scheduler.Debug(); debug.getQueue().then((jobs: scheduler.Job[]) => { @@ -561,6 +611,16 @@ suite('scheduler.ts tests', () => { }); }); + test('Debug :: getJobErrorPath', () => { + let debug = new scheduler.Debug(); + + debug.getQueue().then((jobs: scheduler.Job[]) => { + jobs.forEach((job: scheduler.Job) => { + assert.strictEqual(job.errorFile, debug.getJobErrorPath(job)); + }); + }); + }); + test('getScheduler', async () => { let config = vscode.workspace.getConfiguration('slurm-dashboard');