diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfad1de..d53a849 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,11 @@ on: pull_request: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: [ubuntu-latest, windows-latest] node-version: ['18'] steps: - uses: actions/checkout@v4 diff --git a/.prettierrc.json b/.prettierrc.json index aea847c..d6f3fad 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,6 @@ { "semi": true, "trailingComma": "all", - "singleQuote": true + "singleQuote": true, + "endOfLine": "lf" } diff --git a/.vscode/settings.json b/.vscode/settings.json index f85186b..a9c1403 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,8 @@ "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true + "editor.formatOnSave": true, + + "mochaExplorer.files": "tests/**/*.ts", + "mochaExplorer.require": "ts-node/register" } diff --git a/extension/src/fileAccessor.ts b/extension/src/fileAccessor.ts index 6d199f8..6651606 100644 --- a/extension/src/fileAccessor.ts +++ b/extension/src/fileAccessor.ts @@ -4,30 +4,46 @@ import { FileAccessor } from '../../src/common'; export const workspaceFileAccessor: FileAccessor = { isWindows: typeof process !== 'undefined' && process.platform === 'win32', readFile(path: string): Promise { - const uri = pathToUri(path); + const uri = vscode.Uri.file(path); return thenableToPromise(vscode.workspace.fs.readFile(uri)); }, writeFile(path: string, contents: Uint8Array): Promise { - const uri = pathToUri(path); + const uri = vscode.Uri.file(path); return thenableToPromise(vscode.workspace.fs.writeFile(uri, contents)); }, basename(path: string): string { - const uri = pathToUri(path); + const uri = vscode.Uri.file(path); const lastSlash = uri.path.lastIndexOf('/'); if (lastSlash === -1) { return path; } return uri.path.substring(lastSlash + 1); }, -}; + filePathRelativeTo(base: string, filePath: string): string { + // Check if filePath is an absolute path + if (this.isWindows) { + if (filePath.match(/^[a-zA-Z]:\\/)) { + return filePath; + } + } else { + if (filePath.startsWith('/')) { + return filePath; + } + } -function pathToUri(path: string) { - try { - return vscode.Uri.file(path); - } catch (e) { - return vscode.Uri.parse(path, true); - } -} + // Create a Uri object with the base path + let baseUri = vscode.Uri.file(base); + if (!baseUri.path.endsWith('/')) { + // If the base path is not a directory, get its parent directory + baseUri = vscode.Uri.joinPath(baseUri, '..'); + } + + // Resolve the file path against the base Uri + const fullUri = vscode.Uri.joinPath(baseUri, filePath); + + return fullUri.fsPath; + }, +}; function thenableToPromise(t: Thenable): Promise { return new Promise((resolve, reject) => { diff --git a/package-lock.json b/package-lock.json index 223a044..ba2f2fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "vlq": "^2.0.4" }, "bin": { - "avm-debug": "out/src/cli.js" + "avm-debug-adapter": "out/src/cli.js" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -42,7 +42,7 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^3.0.3", - "rimraf": "^3.0.2", + "shx": "^0.3.4", "ts-mocha": "^10.0.0", "typescript": "^4.6.3", "url": "^0.11.3" @@ -2601,10 +2601,13 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -2804,6 +2807,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2942,6 +2957,15 @@ "dev": true, "optional": true }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2954,6 +2978,18 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4089,6 +4125,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4363,6 +4405,18 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -4390,6 +4444,23 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4510,6 +4581,39 @@ "node": ">=8" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -4698,6 +4802,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", diff --git a/package.json b/package.json index d912209..468689b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "src" ], "scripts": { - "compile": "rimraf out && tsc -p ./ && cp -R algosdk out/algosdk", + "compile": "shx rm -rf out && tsc -p ./ && shx cp -R algosdk out/algosdk", "lint": "eslint src --ext ts", "typecheck": "tsc -p tsconfig.json --noEmit", "check-format": "prettier . --check", @@ -63,7 +63,7 @@ "extension:package": "vsce package", "extension:publish": "vsce publish", "extension:publish-pre-release": "vsce publish --pre-release", - "test": "ts-mocha -p tsconfig.json tests/*test.ts --timeout 30s --diff false", + "test": "ts-mocha -p tsconfig.json 'tests/**/*test.ts' --timeout 30s --diff false", "test:coverage": "nyc npm run test" }, "dependencies": { @@ -97,7 +97,7 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^3.0.3", - "rimraf": "^3.0.2", + "shx": "^0.3.4", "ts-mocha": "^10.0.0", "typescript": "^4.6.3", "url": "^0.11.3" diff --git a/src/common/fileAccessor.ts b/src/common/fileAccessor.ts index bc51ba8..3a51be8 100644 --- a/src/common/fileAccessor.ts +++ b/src/common/fileAccessor.ts @@ -2,5 +2,6 @@ export interface FileAccessor { isWindows: boolean; readFile(path: string): Promise; writeFile(path: string, contents: Uint8Array): Promise; + filePathRelativeTo(base: string, filePath: string): string; basename(path: string): string; } diff --git a/src/common/runtime.ts b/src/common/runtime.ts index 6c4a6d4..ac76f81 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -8,7 +8,11 @@ import { TraceReplayStackFrame, } from './traceReplayEngine'; import { FileAccessor } from './fileAccessor'; -import { AvmDebuggingAssets, ProgramSourceDescriptor } from './utils'; +import { + AvmDebuggingAssets, + ProgramSourceDescriptor, + normalizePathAndCasing, +} from './utils'; export interface IRuntimeBreakpoint { id: number; @@ -249,7 +253,7 @@ export class AvmRuntime extends EventEmitter { line: number, column?: number, ): IRuntimeBreakpoint { - path = this.normalizePathAndCasing(path); + path = normalizePathAndCasing(this.fileAccessor, path); const bp: IRuntimeBreakpoint = { verified: false, @@ -269,7 +273,7 @@ export class AvmRuntime extends EventEmitter { } public clearBreakpoints(path: string): void { - this.breakPoints.delete(this.normalizePathAndCasing(path)); + this.breakPoints.delete(normalizePathAndCasing(this.fileAccessor, path)); } public getAppStateReferences(): number[] { @@ -359,14 +363,6 @@ export class AvmRuntime extends EventEmitter { }, 0); } - private normalizePathAndCasing(filePath: string) { - if (this.fileAccessor.isWindows) { - return filePath.replace(/\//g, '\\').toLowerCase(); - } else { - return filePath.replace(/\\/g, '/'); - } - } - private isFrameLocationOnBreakpoint( location: FrameSourceLocation, bp: IRuntimeBreakpointLocation, @@ -386,7 +382,7 @@ export class AvmRuntime extends EventEmitter { private findSourceDescriptorsForPath( filePath: string, ): Array<{ descriptor: ProgramSourceDescriptor; sourceIndex: number }> { - filePath = this.normalizePathAndCasing(filePath); + filePath = normalizePathAndCasing(this.fileAccessor, filePath); const sourceDescriptors: Array<{ descriptor: ProgramSourceDescriptor; diff --git a/src/common/utils.ts b/src/common/utils.ts index 7f53cfe..768117c 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -15,6 +15,24 @@ export function utf8Decode(data: Uint8Array): string | undefined { } } +/** + * Normalize the given file path. + * + * On Windows, this will replace all forward slashes with backslashes and convert + * the path to lowercase, since Windows paths are case-insensitive. + */ +export function normalizePathAndCasing( + fileAccessor: FileAccessor, + filePath: string, +) { + if (fileAccessor.isWindows) { + // Normalize path to lowercase on Windows, since it is case-insensitive + return filePath.replace(/\//g, '\\').toLowerCase(); + } else { + return filePath.replace(/\\/g, '/'); + } +} + export function limitArray( array: Array, start?: number, @@ -121,10 +139,6 @@ export class ByteArrayMap { } } -function filePathRelativeTo(base: string, filePath: string): string { - return new URL(filePath, new URL(base, 'file://')).pathname; -} - interface ProgramSourceEntryFile { 'txn-group-sources': ProgramSourceEntry[]; } @@ -135,19 +149,23 @@ interface ProgramSourceEntry { } export class ProgramSourceDescriptor { + public readonly fileAccessor: FileAccessor; public readonly sourcemapFileLocation: string; public readonly sourcemap: algosdk.ProgramSourceMap; public readonly hash: Uint8Array; constructor({ + fileAccessor, sourcemapFileLocation, sourcemap, hash, }: { + fileAccessor: FileAccessor; sourcemapFileLocation: string; sourcemap: algosdk.ProgramSourceMap; hash: Uint8Array; }) { + this.fileAccessor = fileAccessor; this.sourcemapFileLocation = sourcemapFileLocation; this.sourcemap = sourcemap; this.hash = hash; @@ -160,9 +178,12 @@ export class ProgramSourceDescriptor { } public getFullSourcePath(index: number): string { - return filePathRelativeTo( - this.sourcemapFileLocation, - this.sourcemap.sources[index], + return normalizePathAndCasing( + this.fileAccessor, + this.fileAccessor.filePathRelativeTo( + this.sourcemapFileLocation, + this.sourcemap.sources[index], + ), ); } @@ -171,9 +192,9 @@ export class ProgramSourceDescriptor { originFile: string, data: ProgramSourceEntry, ): Promise { - const sourcemapFileLocation = filePathRelativeTo( - originFile, - data['sourcemap-location'], + const sourcemapFileLocation = normalizePathAndCasing( + fileAccessor, + fileAccessor.filePathRelativeTo(originFile, data['sourcemap-location']), ); const rawSourcemap = await prefixPotentialError( fileAccessor.readFile(sourcemapFileLocation), @@ -184,6 +205,7 @@ export class ProgramSourceDescriptor { ); return new ProgramSourceDescriptor({ + fileAccessor, sourcemapFileLocation, sourcemap, hash: algosdk.base64ToBytes(data.hash), diff --git a/src/node/fileAccessor.ts b/src/node/fileAccessor.ts index 2e01888..5e2ac9f 100644 --- a/src/node/fileAccessor.ts +++ b/src/node/fileAccessor.ts @@ -1,6 +1,7 @@ import { readFile, writeFile } from 'fs/promises'; import { basename } from 'path'; import { FileAccessor } from '../common'; +import * as path from 'path'; export const nodeFileAccessor: FileAccessor = { isWindows: process.platform === 'win32', @@ -13,4 +14,14 @@ export const nodeFileAccessor: FileAccessor = { basename(path: string): string { return basename(path); }, + filePathRelativeTo(base: string, filePath: string): string { + if (path.isAbsolute(filePath)) { + return filePath; + } + if (!base.endsWith(path.sep)) { + // If the base path is not a directory, get its parent directory + base = path.dirname(base); + } + return path.join(base, filePath); + }, }; diff --git a/tests/adapter.test.ts b/tests/adapter.test.ts index 9c3c97e..f5a0b91 100644 --- a/tests/adapter.test.ts +++ b/tests/adapter.test.ts @@ -2,7 +2,8 @@ import * as assert from 'assert'; import * as path from 'path'; import * as algosdk from '../algosdk'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { ByteArrayMap } from '../src/common/utils'; +import { ByteArrayMap, normalizePathAndCasing } from '../src/common/utils'; +import { nodeFileAccessor } from '../src/node'; import { TestFixture, assertVariables, advanceTo, DATA_ROOT } from './testing'; describe('Debug Adapter Tests', () => { @@ -209,9 +210,9 @@ describe('Debug Adapter Tests', () => { describe('setBreakpoints', () => { it('should stop on a breakpoint', async () => { - const PROGRAM = path.join( - DATA_ROOT, - 'app-state-changes/state-changes.teal', + const PROGRAM = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'), ); const BREAKPOINT_LINE = 2; @@ -275,9 +276,9 @@ describe('Debug Adapter Tests', () => { let expectedStepOverLocationsSlotMachine: Location[]; { - const slotMachinePath = path.join( - DATA_ROOT, - 'slot-machine/slot-machine.teal', + const slotMachinePath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'slot-machine/slot-machine.teal'), ); const label5Callsub = [ @@ -484,8 +485,14 @@ describe('Debug Adapter Tests', () => { client.assertStoppedLocation('entry', {}), ]); - const lsigPath = path.join(DATA_ROOT, 'stepping-test/lsig.teal'); - const appPath = path.join(DATA_ROOT, 'stepping-test/app.teal'); + const lsigPath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'stepping-test/lsig.teal'), + ); + const appPath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'stepping-test/app.teal'), + ); const expectedLocations: Location[] = [ { name: 'transaction-group-0.json', @@ -748,9 +755,9 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const programPath = path.join( - DATA_ROOT, - 'slot-machine/slot-machine.teal', + const programPath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'slot-machine/slot-machine.teal'), ); await client.hitBreakpoint( @@ -812,17 +819,17 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const fakeRandomPath = path.join( - DATA_ROOT, - 'slot-machine/fake-random.teal', + const fakeRandomPath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'slot-machine/fake-random.teal'), ); - const randomBytePath = path.join( - DATA_ROOT, - 'slot-machine/random-byte.teal', + const randomBytePath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'slot-machine/random-byte.teal'), ); - const slotMachinePath = path.join( - DATA_ROOT, - 'slot-machine/slot-machine.teal', + const slotMachinePath = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'slot-machine/slot-machine.teal'), ); await client.hitBreakpoint( @@ -1125,7 +1132,10 @@ describe('Debug Adapter Tests', () => { const currentFrame = stackTraceResponse.body.stackFrames[0]; assert.notStrictEqual( currentFrame.source?.path, - path.join(DATA_ROOT, 'slot-machine/slot-machine.teal'), + normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'slot-machine/slot-machine.teal'), + ), 'Program has step locations beyond expected', ); assert.notStrictEqual( @@ -1150,7 +1160,10 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const PROGRAM = path.join(DATA_ROOT, 'stepping-test/lsig.teal'); + const PROGRAM = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'stepping-test/lsig.teal'), + ); await client.hitBreakpoint( { simulateTraceFile, programSourcesDescriptionFile }, @@ -1192,9 +1205,9 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const PROGRAM = path.join( - DATA_ROOT, - 'stack-scratch/stack-scratch.teal', + const PROGRAM = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'stack-scratch/stack-scratch.teal'), ); await client.hitBreakpoint( @@ -1307,9 +1320,9 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const PROGRAM = path.join( - DATA_ROOT, - 'app-state-changes/state-changes.teal', + const PROGRAM = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'), ); await client.hitBreakpoint( @@ -1404,9 +1417,9 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const PROGRAM = path.join( - DATA_ROOT, - 'app-state-changes/state-changes.teal', + const PROGRAM = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'), ); await client.hitBreakpoint( @@ -1537,9 +1550,9 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const PROGRAM = path.join( - DATA_ROOT, - 'app-state-changes/state-changes.teal', + const PROGRAM = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'), ); await client.hitBreakpoint( @@ -1630,7 +1643,10 @@ describe('Debug Adapter Tests', () => { const testSources: SourceInfo[] = [ { - path: path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), + path: normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), + ), validBreakpoints: [ { line: 4, column: 1 }, { line: 4, column: 20 }, @@ -1647,7 +1663,10 @@ describe('Debug Adapter Tests', () => { ], }, { - path: path.join(DATA_ROOT, 'sourcemap-test/lib.teal'), + path: normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'sourcemap-test/lib.teal'), + ), validBreakpoints: [ { line: 2, column: 22 }, { line: 2, column: 26 }, @@ -1808,7 +1827,10 @@ describe('Debug Adapter Tests', () => { const result = await client.setBreakpointsRequest({ source: { - path: path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), + path: normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), + ), }, breakpoints: [ { line: 0, column: 0 }, @@ -1840,7 +1862,10 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const program = path.join(DATA_ROOT, 'errors/inner-app/inner.teal'); + const program = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/inner-app/inner.teal'), + ); await Promise.all([ client.configurationSequence(), @@ -1989,7 +2014,10 @@ describe('Debug Adapter Tests', () => { // And backwards again await client.stepBackRequest({ threadId: 1 }); await client.assertStoppedLocation('step', { - path: path.join(DATA_ROOT, 'errors/inner-app/outer.teal'), + path: normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/inner-app/outer.teal'), + ), line: 12, column: 1, }); @@ -2020,7 +2048,10 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const program = path.join(DATA_ROOT, 'errors/logicsig/lsig-err.teal'); + const program = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/logicsig/lsig-err.teal'), + ); await Promise.all([ client.configurationSequence(), @@ -2110,13 +2141,13 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const lsigProgram = path.join( - DATA_ROOT, - 'errors/app-from-logicsig/nine.teal', + const lsigProgram = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/app-from-logicsig/nine.teal'), ); - const appProgram = path.join( - DATA_ROOT, - 'errors/app-from-logicsig/inner.teal', + const appProgram = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/app-from-logicsig/inner.teal'), ); await client.hitBreakpoint( @@ -2164,13 +2195,13 @@ describe('Debug Adapter Tests', () => { ); const { client } = fixture; - const lsigProgram = path.join( - DATA_ROOT, - 'errors/logicsig-after-error/nine.teal', + const lsigProgram = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/logicsig-after-error/nine.teal'), ); - const appProgram = path.join( - DATA_ROOT, - 'errors/logicsig-after-error/inner.teal', + const appProgram = normalizePathAndCasing( + nodeFileAccessor, + path.join(DATA_ROOT, 'errors/logicsig-after-error/inner.teal'), ); await Promise.all([ diff --git a/tests/node/fileAccessor.test.ts b/tests/node/fileAccessor.test.ts new file mode 100644 index 0000000..9150715 --- /dev/null +++ b/tests/node/fileAccessor.test.ts @@ -0,0 +1,73 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { nodeFileAccessor } from '../../src/node'; + +describe('Node FileAccessor Tests', () => { + describe('filePathRelativeTo', () => { + function getExampleAbsolutePathFor(filename: string): string { + let base: string; + if (nodeFileAccessor.isWindows) { + base = 'c:\\somelongpath\\sources'; + } else { + base = '/somelongpath/sources'; + } + return path.join(base, filename); + } + + interface TestCase { + description: string; + base: string; + filePath: string; + expected: string; + } + + const testCases: TestCase[] = [ + { + description: 'should be correct when the base path is a file', + base: getExampleAbsolutePathFor('sources.json'), + filePath: 'slot-machine/slot-machine.teal.tok.map', + expected: getExampleAbsolutePathFor( + ['slot-machine', 'slot-machine.teal.tok.map'].join(path.sep), + ), + }, + { + description: 'should be correct when the relative path contains ..', + base: getExampleAbsolutePathFor('sources.json'), + filePath: '../slot-machine/slot-machine.teal.tok.map', + expected: getExampleAbsolutePathFor( + ['..', 'slot-machine', 'slot-machine.teal.tok.map'].join(path.sep), + ), + }, + { + description: 'should be correct when the base path is a directory', + base: getExampleAbsolutePathFor('directory' + path.sep), + filePath: 'slot-machine/slot-machine.teal.tok.map', + expected: getExampleAbsolutePathFor( + ['directory', 'slot-machine', 'slot-machine.teal.tok.map'].join( + path.sep, + ), + ), + }, + { + description: 'should be correct when the file path is absolute', + base: 'i do not matter', + filePath: getExampleAbsolutePathFor( + 'slot-machine/slot-machine.teal.tok.map', + ), + expected: getExampleAbsolutePathFor( + 'slot-machine/slot-machine.teal.tok.map', + ), + }, + ]; + + for (const testCase of testCases) { + it(testCase.description, () => { + const result = nodeFileAccessor.filePathRelativeTo( + testCase.base, + testCase.filePath, + ); + assert.strictEqual(result, testCase.expected); + }); + } + }); +});