-
-
Notifications
You must be signed in to change notification settings - Fork 453
fix: enforce valid sketch folder name on Save as
#1821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import * as remote from '@theia/core/electron-shared/@electron/remote'; | ||
import { Dialog } from '@theia/core/lib/browser/dialogs'; | ||
import { nls } from '@theia/core/lib/common/nls'; | ||
import { Deferred } from '@theia/core/lib/common/promise-util'; | ||
import { injectable } from '@theia/core/shared/inversify'; | ||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; | ||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; | ||
import { Sketch, SketchContribution, URI } from './contribution'; | ||
import { SaveAsSketch } from './save-as-sketch'; | ||
|
||
@injectable() | ||
export class ValidateSketch extends SketchContribution { | ||
override onReady(): void { | ||
this.validate(); | ||
} | ||
|
||
private async validate(): Promise<void> { | ||
const result = await this.promptFixActions(); | ||
if (!result) { | ||
const yes = await this.prompt( | ||
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'), | ||
nls.localize( | ||
'arduino/validateSketch/abortFixMessage', | ||
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.", | ||
Dialog.NO | ||
), | ||
[Dialog.NO, Dialog.YES] | ||
); | ||
if (yes) { | ||
return this.validate(); | ||
} | ||
const sketch = await this.sketchService.createNewSketch(); | ||
this.workspaceService.open(new URI(sketch.uri), { | ||
preserveWindow: true, | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Returns with an array of actions the user has to perform to fix the invalid sketch. | ||
*/ | ||
private validateSketch(sketch: Sketch): FixAction[] { | ||
// sketch folder + main sketch file (requires `Save as...` and window reload) | ||
const sketchFolderName = new URI(sketch.uri).path.base; | ||
const sketchFolderNameError = | ||
Sketch.validateSketchFolderName(sketchFolderName); | ||
if (sketchFolderNameError) { | ||
return [ | ||
{ | ||
execute: async () => { | ||
const unknown = | ||
(await this.promptRenameSketch(sketch)) && | ||
(await this.commandService.executeCommand( | ||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id, | ||
<SaveAsSketch.Options>{ | ||
markAsRecentlyOpened: true, | ||
openAfterMove: true, | ||
wipeOriginal: true, | ||
} | ||
)); | ||
return !!unknown; | ||
}, | ||
}, | ||
]; | ||
} | ||
|
||
// sketch code files (does not require window reload) | ||
return Sketch.uris(sketch) | ||
.filter((uri) => uri !== sketch.mainFileUri) | ||
.map((uri) => new URI(uri)) | ||
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext)) | ||
.map((uri) => ({ | ||
uri, | ||
error: Sketch.validateSketchFolderName(uri.path.name), | ||
})) | ||
.filter(({ error }) => Boolean(error)) | ||
.map(({ uri }) => ({ | ||
execute: async () => { | ||
const unknown = | ||
(await this.promptRenameSketchFile(uri)) && | ||
(await this.commandService.executeCommand( | ||
WorkspaceCommands.FILE_RENAME.id, | ||
uri | ||
)); | ||
return !!unknown; | ||
}, | ||
})); | ||
} | ||
|
||
private async currentSketch(): Promise<Sketch> { | ||
const sketch = this.sketchServiceClient.tryGetCurrentSketch(); | ||
if (CurrentSketch.isValid(sketch)) { | ||
return sketch; | ||
} | ||
const deferred = new Deferred<Sketch>(); | ||
const disposable = this.sketchServiceClient.onCurrentSketchDidChange( | ||
(sketch) => { | ||
if (CurrentSketch.isValid(sketch)) { | ||
disposable.dispose(); | ||
deferred.resolve(sketch); | ||
} | ||
} | ||
); | ||
return deferred.promise; | ||
} | ||
|
||
private async promptFixActions(): Promise<boolean> { | ||
const sketch = await this.currentSketch(); | ||
const fixActions = this.validateSketch(sketch); | ||
for (const fixAction of fixActions) { | ||
const result = await fixAction.execute(); | ||
if (!result) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
private async promptRenameSketch(sketch: Sketch): Promise<boolean> { | ||
return this.prompt( | ||
nls.localize( | ||
'arduino/validateSketch/renameSketchFolderTitle', | ||
'Invalid sketch name' | ||
), | ||
nls.localize( | ||
'arduino/validateSketch/renameSketchFolderMessage', | ||
"The sketch '{0}' cannot be used. Sketch names must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters. To get rid of this message, rename the sketch. Do you want to rename the sketch now?", | ||
sketch.name | ||
) | ||
); | ||
} | ||
|
||
private async promptRenameSketchFile(uri: URI): Promise<boolean> { | ||
return this.prompt( | ||
nls.localize( | ||
'arduino/validateSketch/renameSketchFileTitle', | ||
'Invalid sketch filename' | ||
), | ||
nls.localize( | ||
'arduino/validateSketch/renameSketchFileMessage', | ||
"The sketch file '{0}' cannot be used. Sketch filenames must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters without the file extension. To get rid of this message, rename the sketch file. Do you want to rename the sketch file now?", | ||
uri.path.base | ||
) | ||
); | ||
} | ||
|
||
private async prompt( | ||
title: string, | ||
message: string, | ||
buttons: string[] = [Dialog.CANCEL, Dialog.OK] | ||
): Promise<boolean> { | ||
const { response } = await remote.dialog.showMessageBox( | ||
remote.getCurrentWindow(), | ||
{ | ||
title, | ||
message, | ||
type: 'warning', | ||
buttons, | ||
} | ||
); | ||
// cancel | ||
if (response === 0) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
} | ||
|
||
interface FixAction { | ||
execute(): Promise<boolean>; | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,4 +1,5 @@ | ||||||
import { ApplicationError } from '@theia/core/lib/common/application-error'; | ||||||
import { nls } from '@theia/core/lib/common/nls'; | ||||||
import URI from '@theia/core/lib/common/uri'; | ||||||
|
||||||
export namespace SketchesError { | ||||||
|
@@ -151,6 +152,51 @@ export interface Sketch extends SketchRef { | |||||
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file) | ||||||
} | ||||||
export namespace Sketch { | ||||||
// (non-API) exported for the tests | ||||||
export const invalidSketchFolderNameMessage = nls.localize( | ||||||
'arduino/sketch/invalidSketchName', | ||||||
'Sketch names must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters.' | ||||||
); | ||||||
const invalidCloudSketchFolderNameMessage = nls.localize( | ||||||
'arduino/sketch/invalidCloudSketchName', | ||||||
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.' | ||||||
); | ||||||
/** | ||||||
* `undefined` if the candidate sketch folder name is valid. Otherwise, the validation error message. | ||||||
* Based on the [specs](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-folders-and-files). | ||||||
*/ | ||||||
export function validateSketchFolderName( | ||||||
candidate: string | ||||||
): string | undefined { | ||||||
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,62}$/.test(candidate) | ||||||
? undefined | ||||||
: invalidSketchFolderNameMessage; | ||||||
} | ||||||
|
||||||
/** | ||||||
* `undefined` if the candidate cloud sketch folder name is valid. Otherwise, the validation error message. | ||||||
* Based on how https://create.arduino.cc/editor/ works. | ||||||
*/ | ||||||
export function validateCloudSketchFolderName( | ||||||
candidate: string | ||||||
): string | undefined { | ||||||
return /^[0-9a-zA-Z_]{1,36}$/.test(candidate) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I'm ready to accept that there is some hypothetical reason why the maximum length must be more restrictive for Cloud sketch. At least in this case a valid cloud sketch name length is still in compliance with the specification, even though the converse is not true. But it is unacceptable to allow Cloud sketch names that are in violation of the Arduino Sketch Specification. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It was requested in one of the design files that IDE2 must rename sketches when cloning from and pushing to the cloud. We need to add tests to verify IDE2 can do the name transformation. Users can still create and pull a remote sketch, so relaxing the regex here might not help. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think it is a problem because the new approach is to validate names when the sketch is opened. So even if an invalid sketch is pulled, it will still be at the time the user opens the pulled sketch. I'm referring to this feature:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You're correct, but it only covers some user cases. After IDE2 pulls the cloud sketch and fixes any possible sketch folder name and code file name errors, the user can create a file remotely and pull it again. It will be spec incompatible if the name of the code file starts with an underscore. Please correct me if I am overlooking something. Thanks! IDE2 may run a validation after pulling from the remote. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You are right that this would provide a path to having a non-compliant sketch open in Arduino IDE. However, that will only be possible during that session. The next time the user tries to open the sketch, the added file will be validated. So I wouldn't consider this scenario to completely invalidate the proposed approach of only doing the filename validation on an open operation. Ideally validation would also be done when a file is added or renamed through an "external" process such as a pull. There is an equivalent scenario for local sketches in that the user may add or rename files of the open sketch externally. But I don't consider such validation to be absolutely essential. The most important reason for validating against the specification is to ensure shared sketches are compatible with any official Arduino development tool as well as 3rd party tools from conscientious developers. I think it would be rare that a developer would not end up later opening the type of sketch that is shared (as opposed to a quick throwaway test sketch we might write and use during a single IDE session and never look at again). So if it is relatively easy to add a validation triggered by external addition or name change of sketch file, then great. But if this is something of significant difficulty to implement then I think the "on open" approach is sufficient. |
||||||
? undefined | ||||||
: invalidCloudSketchFolderNameMessage; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Transforms the valid local sketch name into a valid cloud sketch name by replacing dots and dashes with underscore and trimming the length after 36 characters. | ||||||
* Throws an error if `candidate` is not valid. | ||||||
*/ | ||||||
export function toValidCloudSketchFolderName(candidate: string): string { | ||||||
const errorMessage = validateSketchFolderName(candidate); | ||||||
if (errorMessage) { | ||||||
throw new Error(errorMessage); | ||||||
} | ||||||
return candidate.replace(/\./g, '_').replace(/-/g, '_').slice(0, 36); | ||||||
} | ||||||
|
||||||
export function is(arg: unknown): arg is Sketch { | ||||||
if (!SketchRef.is(arg)) { | ||||||
return false; | ||||||
|
@@ -172,7 +218,8 @@ export namespace Sketch { | |||||
return false; | ||||||
} | ||||||
export namespace Extensions { | ||||||
export const MAIN = ['.ino', '.pde']; | ||||||
export const DEFAULT = '.ino'; | ||||||
export const MAIN = [DEFAULT, '.pde']; | ||||||
export const SOURCE = ['.c', '.cpp', '.S']; | ||||||
export const CODE_FILES = [...MAIN, ...SOURCE, '.h', '.hh', '.hpp']; | ||||||
export const ADDITIONAL = [...CODE_FILES, '.json', '.md', '.adoc']; | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; | ||
const disableJSDOM = enableJSDOM(); | ||
|
||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; | ||
FrontendApplicationConfigProvider.set({}); | ||
|
||
import { | ||
FrontendApplication, | ||
LabelProvider, | ||
OpenerService, | ||
} from '@theia/core/lib/browser'; | ||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; | ||
import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; | ||
import { CommandService } from '@theia/core/lib/common/command'; | ||
import { MessageService } from '@theia/core/lib/common/message-service'; | ||
import { nls } from '@theia/core/lib/common/nls'; | ||
import { OS } from '@theia/core/lib/common/os'; | ||
import { SelectionService } from '@theia/core/lib/common/selection-service'; | ||
import URI from '@theia/core/lib/common/uri'; | ||
import { Container } from '@theia/core/shared/inversify'; | ||
import { FileDialogService } from '@theia/filesystem/lib/browser'; | ||
import { FileService } from '@theia/filesystem/lib/browser/file-service'; | ||
import { FileStat } from '@theia/filesystem/lib/common/files'; | ||
import { WorkspaceCompareHandler } from '@theia/workspace/lib/browser/workspace-compare-handler'; | ||
import { WorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler'; | ||
import { WorkspaceDuplicateHandler } from '@theia/workspace/lib/browser/workspace-duplicate-handler'; | ||
import { WorkspacePreferences } from '@theia/workspace/lib/browser/workspace-preferences'; | ||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; | ||
import { expect } from 'chai'; | ||
import { | ||
invalidExtension as invalidExtensionMessage, | ||
parseFileInput, | ||
WorkspaceCommandContribution, | ||
} from '../../browser/theia/workspace/workspace-commands'; | ||
import { Sketch, SketchesService } from '../../common/protocol'; | ||
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl'; | ||
|
||
disableJSDOM(); | ||
|
||
describe('workspace-commands', () => { | ||
describe('parseFileInput', () => { | ||
it("should parse input without extension as '.ino'", () => { | ||
const actual = parseFileInput('foo'); | ||
expect(actual).to.be.deep.equal({ | ||
raw: 'foo', | ||
name: 'foo', | ||
extension: '.ino', | ||
}); | ||
}); | ||
it("should parse input with a trailing dot as '.ino'", () => { | ||
const actual = parseFileInput('foo.'); | ||
expect(actual).to.be.deep.equal({ | ||
raw: 'foo.', | ||
name: 'foo', | ||
extension: '.ino', | ||
}); | ||
}); | ||
it('should parse input with a valid extension', () => { | ||
const actual = parseFileInput('lib.cpp'); | ||
expect(actual).to.be.deep.equal({ | ||
raw: 'lib.cpp', | ||
name: 'lib', | ||
extension: '.cpp', | ||
}); | ||
}); | ||
it('should calculate the file extension based on the last dot index', () => { | ||
const actual = parseFileInput('lib.ino.x'); | ||
expect(actual).to.be.deep.equal({ | ||
raw: 'lib.ino.x', | ||
name: 'lib.ino', | ||
extension: '.x', | ||
}); | ||
}); | ||
it('should ignore trailing spaces after the last dot', () => { | ||
const actual = parseFileInput(' foo. '); | ||
expect(actual).to.be.deep.equal({ | ||
raw: ' foo. ', | ||
name: ' foo', | ||
extension: '.ino', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('validateFileName', () => { | ||
const child: FileStat = { | ||
isFile: true, | ||
isDirectory: false, | ||
isSymbolicLink: false, | ||
resource: new URI('sketch/sketch.ino'), | ||
name: 'sketch.ino', | ||
}; | ||
const parent: FileStat = { | ||
isFile: false, | ||
isDirectory: true, | ||
isSymbolicLink: false, | ||
resource: new URI('sketch'), | ||
name: 'sketch', | ||
children: [child], | ||
}; | ||
|
||
let workspaceCommands: WorkspaceCommandContribution; | ||
const trimmedName = (name: string) => | ||
workspaceCommands['trimFileName'](name); | ||
|
||
async function testMe(userInput: string): Promise<string> { | ||
return workspaceCommands['validateFileName'](userInput, parent); | ||
} | ||
|
||
function createContainer(): Container { | ||
const container = new Container(); | ||
container.bind(FileDialogService).toConstantValue(<FileDialogService>{}); | ||
container.bind(FileService).toConstantValue(<FileService>{ | ||
async exists(resource: URI): Promise<boolean> { | ||
return ( | ||
resource.path.base.includes('_sketch') || | ||
resource.path.base.includes('sketch') | ||
); | ||
}, | ||
}); | ||
container | ||
.bind(FrontendApplication) | ||
.toConstantValue(<FrontendApplication>{}); | ||
container.bind(LabelProvider).toConstantValue(<LabelProvider>{}); | ||
container.bind(MessageService).toConstantValue(<MessageService>{}); | ||
container.bind(OpenerService).toConstantValue(<OpenerService>{}); | ||
container.bind(SelectionService).toConstantValue(<SelectionService>{}); | ||
container.bind(WorkspaceCommandContribution).toSelf().inSingletonScope(); | ||
container | ||
.bind(WorkspaceCompareHandler) | ||
.toConstantValue(<WorkspaceCompareHandler>{}); | ||
container | ||
.bind(WorkspaceDeleteHandler) | ||
.toConstantValue(<WorkspaceDeleteHandler>{}); | ||
container | ||
.bind(WorkspaceDuplicateHandler) | ||
.toConstantValue(<WorkspaceDuplicateHandler>{}); | ||
container | ||
.bind(WorkspacePreferences) | ||
.toConstantValue(<WorkspacePreferences>{}); | ||
container.bind(WorkspaceService).toConstantValue(<WorkspaceService>{}); | ||
container.bind(ClipboardService).toConstantValue(<ClipboardService>{}); | ||
container.bind(ApplicationServer).toConstantValue(<ApplicationServer>{ | ||
async getBackendOS(): Promise<OS.Type> { | ||
return OS.type(); | ||
}, | ||
}); | ||
container.bind(CommandService).toConstantValue(<CommandService>{}); | ||
container.bind(SketchesService).toConstantValue(<SketchesService>{}); | ||
container | ||
.bind(SketchesServiceClientImpl) | ||
.toConstantValue(<SketchesServiceClientImpl>{}); | ||
return container; | ||
} | ||
|
||
beforeEach(() => { | ||
workspaceCommands = createContainer().get<WorkspaceCommandContribution>( | ||
WorkspaceCommandContribution | ||
); | ||
}); | ||
|
||
it("should validate input string without an extension as an '.ino' file", async () => { | ||
const actual = await testMe('valid'); | ||
expect(actual).to.be.empty; | ||
}); | ||
|
||
it('code files cannot start with number (no extension)', async () => { | ||
const actual = await testMe('_invalid'); | ||
expect(actual).to.be.equal(Sketch.invalidSketchFolderNameMessage); | ||
}); | ||
|
||
it('code files cannot start with number (trailing dot)', async () => { | ||
const actual = await testMe('_invalid.'); | ||
expect(actual).to.be.equal(Sketch.invalidSketchFolderNameMessage); | ||
}); | ||
|
||
it('code files cannot start with number (trailing dot)', async () => { | ||
const actual = await testMe('_invalid.cpp'); | ||
expect(actual).to.be.equal(Sketch.invalidSketchFolderNameMessage); | ||
}); | ||
|
||
it('should warn about invalid extension first', async () => { | ||
const actual = await testMe('_invalid.xxx'); | ||
expect(actual).to.be.equal(invalidExtensionMessage('.xxx')); | ||
}); | ||
|
||
it('should not warn about invalid file extension for empty input', async () => { | ||
const actual = await testMe(''); | ||
expect(actual).to.be.equal(Sketch.invalidSketchFolderNameMessage); | ||
}); | ||
|
||
it('should ignore non-code filename validation from the spec', async () => { | ||
const actual = await testMe('_invalid.json'); | ||
expect(actual).to.be.empty; | ||
}); | ||
|
||
it('non-code files should be validated against default new file validation rules', async () => { | ||
const name = ' invalid.json'; | ||
const actual = await testMe(name); | ||
const expected = nls.localizeByDefault( | ||
'Leading or trailing whitespace detected in file or folder name.' | ||
); | ||
expect(actual).to.be.equal(expected); | ||
}); | ||
|
||
it('should warn about existing resource', async () => { | ||
const name = 'sketch.ino'; | ||
const actual = await testMe(name); | ||
const expected = nls.localizeByDefault( | ||
'A file or folder **{0}** already exists at this location. Please choose a different name.', | ||
trimmedName(name) | ||
); | ||
expect(actual).to.be.equal(expected); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { expect } from 'chai'; | ||
import { Sketch } from '../../common/protocol'; | ||
|
||
describe('sketch', () => { | ||
describe('validateSketchFolderName', () => { | ||
( | ||
[ | ||
['sketch', true], | ||
['can-contain-slash-and-dot.ino', true], | ||
['regex++', false], | ||
['dots...', true], | ||
['No Spaces', false], | ||
['_invalidToStartWithUnderscore', false], | ||
['Invalid+Char.ino', false], | ||
['', false], | ||
['/', false], | ||
['//trash/', false], | ||
[ | ||
'63Length_012345678901234567890123456789012345678901234567890123', | ||
true, | ||
], | ||
[ | ||
'TooLong__0123456789012345678901234567890123456789012345678901234', | ||
false, | ||
], | ||
] as [string, boolean][] | ||
).map(([input, expected]) => { | ||
it(`'${input}' should ${ | ||
!expected ? 'not ' : '' | ||
}be a valid sketch folder name`, () => { | ||
const actual = Sketch.validateSketchFolderName(input); | ||
if (expected) { | ||
expect(actual).to.be.undefined; | ||
} else { | ||
expect(actual).to.be.not.undefined; | ||
expect(actual?.length).to.be.greaterThan(0); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
describe('validateCloudSketchFolderName', () => { | ||
( | ||
[ | ||
['sketch', true], | ||
['no-dashes', false], | ||
['no-dots', false], | ||
['No Spaces', false], | ||
['_canStartWithUnderscore', true], | ||
['Invalid+Char.ino', false], | ||
['', false], | ||
['/', false], | ||
['//trash/', false], | ||
['36Length_012345678901234567890123456', true], | ||
['TooLong__0123456789012345678901234567', false], | ||
] as [string, boolean][] | ||
).map(([input, expected]) => { | ||
it(`'${input}' should ${ | ||
!expected ? 'not ' : '' | ||
}be a valid cloud sketch folder name`, () => { | ||
const actual = Sketch.validateCloudSketchFolderName(input); | ||
if (expected) { | ||
expect(actual).to.be.undefined; | ||
} else { | ||
expect(actual).to.be.not.undefined; | ||
expect(actual?.length).to.be.greaterThan(0); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
describe('toValidCloudSketchFolderName', () => { | ||
( | ||
[ | ||
['sketch', 'sketch'], | ||
['can-contain-slash-and-dot.ino', 'can_contain_slash_and_dot_ino'], | ||
['regex++'], | ||
['dots...', 'dots___'], | ||
['No Spaces'], | ||
['_invalidToStartWithUnderscore'], | ||
['Invalid+Char.ino'], | ||
[''], | ||
['/'], | ||
['//trash/'], | ||
[ | ||
'63Length_012345678901234567890123456789012345678901234567890123', | ||
'63Length_012345678901234567890123456', | ||
], | ||
['TooLong__0123456789012345678901234567890123456789012345678901234'], | ||
] as [string, string?][] | ||
).map(([input, expected]) => { | ||
it(`'${input}' should ${expected ? '' : 'not '}map the ${ | ||
!expected ? 'invalid ' : '' | ||
}sketch folder name to a valid cloud sketch folder name${ | ||
expected ? `: '${expected}'` : '' | ||
}`, () => { | ||
if (!expected) { | ||
try { | ||
Sketch.toValidCloudSketchFolderName(input); | ||
throw new Error( | ||
`Expected an error when mapping ${input} to a valid sketch folder name.` | ||
); | ||
} catch (err) { | ||
if (err instanceof Error) { | ||
expect(err.message).to.be.equal( | ||
Sketch.invalidSketchFolderNameMessage | ||
); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
} else { | ||
const actual = Sketch.toValidCloudSketchFolderName(input); | ||
expect(actual).to.be.equal(expected); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to get a confirmation that it really is necessary to disallow the
-
and.
characters in Cloud sketches. That seems a completely unnecessary departure from the Arduino Sketch Specification to me.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started an internal dialog.