From c8174ce160e8c57f1a6b0e87bf86ce56903c2e32 Mon Sep 17 00:00:00 2001 From: Samuel Vogelsanger Date: Thu, 3 Aug 2023 16:23:30 +0200 Subject: [PATCH 1/6] feat: add pfsPathArb and start tests for getRemoteChanges --- src/domain/FileSystem/Change.ts | 6 +- src/domain/FileSystem/Conflict.ts | 1 + src/domain/FileSystem/Path.tests.ts | 50 ++++++++ src/domain/ProjectSync/apiStack.ts | 2 +- .../ProjectSync/getRemoteChanges.tests.ts | 110 ++++++++++++++++++ 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/domain/FileSystem/Path.tests.ts create mode 100644 src/domain/ProjectSync/getRemoteChanges.tests.ts diff --git a/src/domain/FileSystem/Change.ts b/src/domain/FileSystem/Change.ts index 31ec0ddf..cd21cadc 100644 --- a/src/domain/FileSystem/Change.ts +++ b/src/domain/FileSystem/Change.ts @@ -5,7 +5,7 @@ import { LocalFileInfo } from './LocalFileInfo'; import { RemoteFileInfo, RemoteNodeInfo } from './RemoteNodeInfo'; export type RemoteChangeType = - | tagged.Tagged<'noChange'> + | tagged.Tagged<'noChange'> // fixme: seems to be unused - remove | tagged.Tagged<'updated', number> | tagged.Tagged<'removed'> | tagged.Tagged<'added', number>; @@ -13,7 +13,7 @@ export type RemoteChangeType = export const remoteChangeType = tagged.build(); export type LocalChangeType = - | tagged.Tagged<'noChange'> + | tagged.Tagged<'noChange'> // fixme: seems to be unused - remove | tagged.Tagged<'updated'> | tagged.Tagged<'removed'> | tagged.Tagged<'added'>; @@ -73,7 +73,7 @@ export const getRemoteChanges = ( array.map(({ type, path, version }) => ({ type, path, - change: remoteChangeType.updated(version), + change: remoteChangeType.updated(version), // fixme: use latest version here })), ); return pipe( diff --git a/src/domain/FileSystem/Conflict.ts b/src/domain/FileSystem/Conflict.ts index 94b9b3ef..f9c36a34 100644 --- a/src/domain/FileSystem/Conflict.ts +++ b/src/domain/FileSystem/Conflict.ts @@ -22,6 +22,7 @@ export const getConflicts = ( remote, array.findFirst((changeRemote) => eqFsNode.equals(changeLocal, changeRemote)), option.map(({ change }) => change), + // fixme: don't do this: instead, replace array.intersection with a groupBy(getPath) option.getOrElseW(() => remoteChangeType.noChange()), ), })), diff --git a/src/domain/FileSystem/Path.tests.ts b/src/domain/FileSystem/Path.tests.ts new file mode 100644 index 00000000..cfb8c809 --- /dev/null +++ b/src/domain/FileSystem/Path.tests.ts @@ -0,0 +1,50 @@ +import { fc } from '@code-expert/test-utils'; +import { assert, describe, it } from 'vitest'; +import { iots } from '@code-expert/prelude'; +import { PfsPath, PfsPathBrand, isValidDirName, isValidFileName } from '@/domain/FileSystem'; + +const wordCharBuilders = [ + { num: 26, build: (v: number) => String.fromCharCode(v + 0x41) }, + { num: 26, build: (v: number) => String.fromCharCode(v + 0x61) }, + { num: 10, build: (v: number) => String.fromCharCode(v + 0x30) }, + { num: 1, build: () => String.fromCharCode(0x5f) }, +]; + +const fsNodeCharBuilders = [ + ...wordCharBuilders, + { num: 0, build: () => String.fromCharCode(0x20) }, + { num: 1, build: () => String.fromCharCode(0x2d) }, +]; + +export const legalDirNameArb = fc.stringOf(fc.mapToConstant(...fsNodeCharBuilders), { + minLength: 1, + maxLength: 80, +}); + +export const legalFileNameArb = fc + .tuple( + fc.stringOf(fc.mapToConstant(...fsNodeCharBuilders), { minLength: 0, maxLength: 80 }), + fc.constant('.'), + fc.stringOf(fc.mapToConstant(...wordCharBuilders), { minLength: 1, maxLength: 5 }), + ) + .map((elements) => elements.join('')); + +export const pfsPathArb: fc.Arbitrary = fc + .tuple(fc.array(legalDirNameArb), legalFileNameArb) + .map(([path, file]) => `./${path.join('/')}/${file}`) + .map(iots.brandFromLiteral); + +describe('PfsPath', () => { + describe('legalDirNameArb', () => { + it('should only produce strings that are accepted by isValidDirName', () => { + fc.assert(fc.property(legalDirNameArb, (dirName) => assert.isTrue(isValidDirName(dirName)))); + }); + }); + describe('legalFileNameArb', () => { + it('should only produce strings that are accepted by isValidFileName', () => { + fc.assert( + fc.property(legalFileNameArb, (fileName) => assert.isTrue(isValidFileName(fileName))), + ); + }); + }); +}); diff --git a/src/domain/ProjectSync/apiStack.ts b/src/domain/ProjectSync/apiStack.ts index 99257ec1..29a02076 100644 --- a/src/domain/ProjectSync/apiStack.ts +++ b/src/domain/ProjectSync/apiStack.ts @@ -20,7 +20,7 @@ export const apiStack: ApiStack = { readRemoteProjectFile: (projectId, file) => apiGetSigned({ path: `project/${projectId}/file`, - jwtPayload: { path: file }, + jwtPayload: { path: file }, // fixme: add version codec: iots.Uint8ArrayC, responseType: ResponseType.Binary, }), diff --git a/src/domain/ProjectSync/getRemoteChanges.tests.ts b/src/domain/ProjectSync/getRemoteChanges.tests.ts new file mode 100644 index 00000000..e2d6bf8f --- /dev/null +++ b/src/domain/ProjectSync/getRemoteChanges.tests.ts @@ -0,0 +1,110 @@ +import { fc } from '@code-expert/test-utils'; +import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; +import * as nodePath from 'path'; +import { assert, describe, it } from 'vitest'; +import { nonEmptyArray, option, pipe, task, taskEither } from '@code-expert/prelude'; +import { + RemoteDirInfo, + RemoteFileChange, + RemoteFileInfo, + getRemoteChanges, + remoteChangeType, +} from '@/domain/FileSystem'; +import { pfsPathArb } from '@/domain/FileSystem/Path.tests'; +import { FileSystemStack } from '@/domain/FileSystem/fileSystemStack'; +import { escape } from '@/lib/tauri/path'; +import { panic } from '@/utils/error'; + +export const nodeFsStack: FileSystemStack = { + dirname: (path) => taskEither.fromIO(() => nodePath.dirname(path)), + join: (...paths) => task.fromIO(() => nodePath.join(...paths)), + stripAncestor: (ancestor) => (to) => + taskEither.fromIO(() => { + const relative = nodePath.relative(ancestor, to); + const normalized = nodePath.normalize(relative); + return nodePath.format({ dir: '.', base: normalized }); + }), + escape, + getFileHash: () => panic('getFileHash is not implemented on nodeFsStack'), + removeFile: () => panic('removeFile is not implemented on nodeFsStack'), +}; + +const remoteFileInfoArb = fc.record({ + type: fc.constant('file'), + path: pfsPathArb, + version: fc.nat(), + permissions: fc.constantFrom('r', 'rw'), +}); + +const remoteDirInfoArb = fc.record({ + type: fc.constant('dir'), + path: pfsPathArb, + version: fc.nat(), + permissions: fc.constantFrom('r', 'rw'), +}); + +describe('getRemoteChanges', () => { + it('should classify missing files as "removed"', () => { + const dirsArb = fc.array(remoteDirInfoArb); + const commonFilesArb = fc.array(remoteFileInfoArb); + const removedFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + + const missingToRemoved = ( + dirs: Array, + commonFiles: Array, + removedFiles: NonEmptyArray, + ) => + assert.deepEqual( + getRemoteChanges([...commonFiles, ...removedFiles], [...commonFiles, ...dirs]), + pipe( + removedFiles, + nonEmptyArray.map( + (fileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.removed(), + }), + ), + option.some, + ), + ); + + fc.assert(fc.property(dirsArb, commonFilesArb, removedFilesArb, missingToRemoved)); + }); + + // it('should classify new files as "added"', () => { + // const previous: Array = []; + // const latest: Array = [ + // { path: iots.brandFromLiteral('./main.py'), type: 'file', version: 1, permissions: 'rw' }, + // ]; + // assert.deepEqual( + // getRemoteChanges(previous, latest), + // option.some>([ + // { + // path: iots.brandFromLiteral('./main.py'), + // type: 'file', + // change: remoteChangeType.added(1), + // }, + // ]), + // ); + // }); + // + // it('should classify files with a different version as "updated"', () => { + // const previous: Array = [ + // { path: iots.brandFromLiteral('./main.py'), type: 'file', version: 1, permissions: 'rw' }, + // ]; + // const latest: Array = [ + // { path: iots.brandFromLiteral('./main.py'), type: 'file', version: 2, permissions: 'rw' }, + // ]; + // assert.deepEqual( + // getRemoteChanges(previous, latest), + // option.some>([ + // { + // path: iots.brandFromLiteral('./main.py'), + // type: 'file', + // change: remoteChangeType.updated(1), + // }, + // ]), + // ); + // }); +}); From ccb1480b4c1bd836fb2a3ec19973c5e7c349c62c Mon Sep 17 00:00:00 2001 From: Samuel Vogelsanger Date: Fri, 4 Aug 2023 17:42:51 +0200 Subject: [PATCH 2/6] feat: add more tests for getRemoteChanges --- .../ProjectSync/getRemoteChanges.tests.ts | 171 ++++++++++++++---- 1 file changed, 131 insertions(+), 40 deletions(-) diff --git a/src/domain/ProjectSync/getRemoteChanges.tests.ts b/src/domain/ProjectSync/getRemoteChanges.tests.ts index e2d6bf8f..31de2e21 100644 --- a/src/domain/ProjectSync/getRemoteChanges.tests.ts +++ b/src/domain/ProjectSync/getRemoteChanges.tests.ts @@ -1,5 +1,6 @@ import { fc } from '@code-expert/test-utils'; import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; +import { Lens } from 'monocle-ts'; import * as nodePath from 'path'; import { assert, describe, it } from 'vitest'; import { nonEmptyArray, option, pipe, task, taskEither } from '@code-expert/prelude'; @@ -7,14 +8,18 @@ import { RemoteDirInfo, RemoteFileChange, RemoteFileInfo, + RemoteNodeInfo, getRemoteChanges, remoteChangeType, } from '@/domain/FileSystem'; import { pfsPathArb } from '@/domain/FileSystem/Path.tests'; import { FileSystemStack } from '@/domain/FileSystem/fileSystemStack'; +import { ordFsNode } from '@/lib/tauri/fs'; import { escape } from '@/lib/tauri/path'; import { panic } from '@/utils/error'; +const Version = Lens.fromProp()('version'); + export const nodeFsStack: FileSystemStack = { dirname: (path) => taskEither.fromIO(() => nodePath.dirname(path)), join: (...paths) => task.fromIO(() => nodePath.join(...paths)), @@ -47,17 +52,17 @@ describe('getRemoteChanges', () => { it('should classify missing files as "removed"', () => { const dirsArb = fc.array(remoteDirInfoArb); const commonFilesArb = fc.array(remoteFileInfoArb); - const removedFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + const missingFilesArb = fc.nonEmptyArray(remoteFileInfoArb); const missingToRemoved = ( dirs: Array, commonFiles: Array, - removedFiles: NonEmptyArray, + missingFiles: NonEmptyArray, ) => assert.deepEqual( - getRemoteChanges([...commonFiles, ...removedFiles], [...commonFiles, ...dirs]), + getRemoteChanges([...commonFiles, ...missingFiles], [...commonFiles, ...dirs]), pipe( - removedFiles, + missingFiles, nonEmptyArray.map( (fileInfo): RemoteFileChange => ({ path: fileInfo.path, @@ -69,42 +74,128 @@ describe('getRemoteChanges', () => { ), ); - fc.assert(fc.property(dirsArb, commonFilesArb, removedFilesArb, missingToRemoved)); + fc.assert(fc.property(dirsArb, commonFilesArb, missingFilesArb, missingToRemoved)); + }); + + it('should classify new files as "added"', () => { + const dirsArb = fc.array(remoteDirInfoArb); + const commonFilesArb = fc.array(remoteFileInfoArb); + const newFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + + const newToAdded = ( + dirs: Array, + commonFiles: Array, + newFiles: NonEmptyArray, + ) => + assert.deepEqual( + getRemoteChanges([...commonFiles], [...commonFiles, ...dirs, ...newFiles]), + pipe( + newFiles, + nonEmptyArray.map( + (fileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.added(fileInfo.version), + }), + ), + option.some, + ), + ); + + fc.assert(fc.property(dirsArb, commonFilesArb, newFilesArb, newToAdded)); + }); + + it('should classify files with a different version as "updated"', () => { + const dirsArb = fc.array(remoteDirInfoArb); + const commonFilesArb = fc.array(remoteFileInfoArb); + const changedFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + + const incrementVersion = Version.modify((v) => v + 1); + const changedToUpdated = ( + dirs: Array, + commonFiles: Array, + changedFiles: NonEmptyArray, + ) => + assert.deepEqual( + getRemoteChanges( + [...commonFiles, ...changedFiles], + [...commonFiles, ...dirs, ...changedFiles.map(incrementVersion)], + ), + pipe( + changedFiles, + nonEmptyArray.map( + (fileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.updated(fileInfo.version), + }), + ), + option.some, + ), + ); + + fc.assert(fc.property(dirsArb, commonFilesArb, changedFilesArb, changedToUpdated)); }); - // it('should classify new files as "added"', () => { - // const previous: Array = []; - // const latest: Array = [ - // { path: iots.brandFromLiteral('./main.py'), type: 'file', version: 1, permissions: 'rw' }, - // ]; - // assert.deepEqual( - // getRemoteChanges(previous, latest), - // option.some>([ - // { - // path: iots.brandFromLiteral('./main.py'), - // type: 'file', - // change: remoteChangeType.added(1), - // }, - // ]), - // ); - // }); - // - // it('should classify files with a different version as "updated"', () => { - // const previous: Array = [ - // { path: iots.brandFromLiteral('./main.py'), type: 'file', version: 1, permissions: 'rw' }, - // ]; - // const latest: Array = [ - // { path: iots.brandFromLiteral('./main.py'), type: 'file', version: 2, permissions: 'rw' }, - // ]; - // assert.deepEqual( - // getRemoteChanges(previous, latest), - // option.some>([ - // { - // path: iots.brandFromLiteral('./main.py'), - // type: 'file', - // change: remoteChangeType.updated(1), - // }, - // ]), - // ); - // }); + it('should do all three at the same time', () => { + const dirsArb = fc.array(remoteDirInfoArb); + const commonFilesArb = fc.array(remoteFileInfoArb); + const missingFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + const newFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + const changedFilesArb = fc.nonEmptyArray(remoteFileInfoArb); + + const incrementVersion = Version.modify((v) => v + 1); + const toRemoved = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.removed(), + }); + const toAdded = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.added(fileInfo.version), + }); + const toUpdated = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.updated(fileInfo.version), + }); + const removedNewChanged = ( + dirs: Array, + commonFiles: Array, + missingFiles: NonEmptyArray, + newFiles: NonEmptyArray, + changedFiles: NonEmptyArray, + ) => + assert.deepEqual( + pipe( + getRemoteChanges( + [...commonFiles, ...missingFiles, ...changedFiles], + [...commonFiles, ...dirs, ...newFiles, ...changedFiles.map(incrementVersion)], + ), + option.map(nonEmptyArray.sort(ordFsNode)), + ), + pipe( + [ + nonEmptyArray.map(toRemoved)(missingFiles), + nonEmptyArray.map(toAdded)(newFiles), + nonEmptyArray.map(toUpdated)(changedFiles), + ], + nonEmptyArray.concatAll(nonEmptyArray.getSemigroup()), + nonEmptyArray.sort(ordFsNode), + option.some, + ), + ); + + fc.assert( + fc.property( + dirsArb, + commonFilesArb, + missingFilesArb, + newFilesArb, + changedFilesArb, + removedNewChanged, + ), + ); + }); }); From aec7241d2827cec43a02a4ff8fc590bea298da82 Mon Sep 17 00:00:00 2001 From: Samuel Vogelsanger Date: Mon, 7 Aug 2023 16:07:50 +0200 Subject: [PATCH 3/6] polish: polish and save additional tasks --- src/domain/FileSystem/Path.tests.ts | 1 + .../ProjectSync/getRemoteChanges.tests.ts | 71 ++++++------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/src/domain/FileSystem/Path.tests.ts b/src/domain/FileSystem/Path.tests.ts index cfb8c809..8caa6f23 100644 --- a/src/domain/FileSystem/Path.tests.ts +++ b/src/domain/FileSystem/Path.tests.ts @@ -29,6 +29,7 @@ export const legalFileNameArb = fc ) .map((elements) => elements.join('')); +// todo: it is unclear how many "collisions" this produces, we might need a better solution export const pfsPathArb: fc.Arbitrary = fc .tuple(fc.array(legalDirNameArb), legalFileNameArb) .map(([path, file]) => `./${path.join('/')}/${file}`) diff --git a/src/domain/ProjectSync/getRemoteChanges.tests.ts b/src/domain/ProjectSync/getRemoteChanges.tests.ts index 31de2e21..0ed640a1 100644 --- a/src/domain/ProjectSync/getRemoteChanges.tests.ts +++ b/src/domain/ProjectSync/getRemoteChanges.tests.ts @@ -19,6 +19,7 @@ import { escape } from '@/lib/tauri/path'; import { panic } from '@/utils/error'; const Version = Lens.fromProp()('version'); +const incrementVersion = Version.modify((v) => v + 1); export const nodeFsStack: FileSystemStack = { dirname: (path) => taskEither.fromIO(() => nodePath.dirname(path)), @@ -49,6 +50,22 @@ const remoteDirInfoArb = fc.record({ }); describe('getRemoteChanges', () => { + const toRemoved = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.removed(), + }); + const toAdded = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.added(fileInfo.version), + }); + const toUpdated = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ + path: fileInfo.path, + type: 'file', + change: remoteChangeType.updated(fileInfo.version), + }); + it('should classify missing files as "removed"', () => { const dirsArb = fc.array(remoteDirInfoArb); const commonFilesArb = fc.array(remoteFileInfoArb); @@ -60,18 +77,9 @@ describe('getRemoteChanges', () => { missingFiles: NonEmptyArray, ) => assert.deepEqual( + // fixme: does concat order matter? how do we make it not matter? getRemoteChanges([...commonFiles, ...missingFiles], [...commonFiles, ...dirs]), - pipe( - missingFiles, - nonEmptyArray.map( - (fileInfo): RemoteFileChange => ({ - path: fileInfo.path, - type: 'file', - change: remoteChangeType.removed(), - }), - ), - option.some, - ), + pipe(missingFiles, nonEmptyArray.map(toRemoved), option.some), ); fc.assert(fc.property(dirsArb, commonFilesArb, missingFilesArb, missingToRemoved)); @@ -89,17 +97,7 @@ describe('getRemoteChanges', () => { ) => assert.deepEqual( getRemoteChanges([...commonFiles], [...commonFiles, ...dirs, ...newFiles]), - pipe( - newFiles, - nonEmptyArray.map( - (fileInfo): RemoteFileChange => ({ - path: fileInfo.path, - type: 'file', - change: remoteChangeType.added(fileInfo.version), - }), - ), - option.some, - ), + pipe(newFiles, nonEmptyArray.map(toAdded), option.some), ); fc.assert(fc.property(dirsArb, commonFilesArb, newFilesArb, newToAdded)); @@ -110,7 +108,6 @@ describe('getRemoteChanges', () => { const commonFilesArb = fc.array(remoteFileInfoArb); const changedFilesArb = fc.nonEmptyArray(remoteFileInfoArb); - const incrementVersion = Version.modify((v) => v + 1); const changedToUpdated = ( dirs: Array, commonFiles: Array, @@ -121,17 +118,7 @@ describe('getRemoteChanges', () => { [...commonFiles, ...changedFiles], [...commonFiles, ...dirs, ...changedFiles.map(incrementVersion)], ), - pipe( - changedFiles, - nonEmptyArray.map( - (fileInfo): RemoteFileChange => ({ - path: fileInfo.path, - type: 'file', - change: remoteChangeType.updated(fileInfo.version), - }), - ), - option.some, - ), + pipe(changedFiles, nonEmptyArray.map(toUpdated), option.some), ); fc.assert(fc.property(dirsArb, commonFilesArb, changedFilesArb, changedToUpdated)); @@ -144,22 +131,6 @@ describe('getRemoteChanges', () => { const newFilesArb = fc.nonEmptyArray(remoteFileInfoArb); const changedFilesArb = fc.nonEmptyArray(remoteFileInfoArb); - const incrementVersion = Version.modify((v) => v + 1); - const toRemoved = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ - path: fileInfo.path, - type: 'file', - change: remoteChangeType.removed(), - }); - const toAdded = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ - path: fileInfo.path, - type: 'file', - change: remoteChangeType.added(fileInfo.version), - }); - const toUpdated = (fileInfo: RemoteFileInfo): RemoteFileChange => ({ - path: fileInfo.path, - type: 'file', - change: remoteChangeType.updated(fileInfo.version), - }); const removedNewChanged = ( dirs: Array, commonFiles: Array, From 0d1d7e2dfeafcdf1ff61a2ef16f327e7cb8ac060 Mon Sep 17 00:00:00 2001 From: Samuel Vogelsanger Date: Mon, 7 Aug 2023 16:08:06 +0200 Subject: [PATCH 4/6] feat: add tests for getLocalChanges --- .../ProjectSync/getLocalChanges.tests.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/domain/ProjectSync/getLocalChanges.tests.ts diff --git a/src/domain/ProjectSync/getLocalChanges.tests.ts b/src/domain/ProjectSync/getLocalChanges.tests.ts new file mode 100644 index 00000000..e27a6902 --- /dev/null +++ b/src/domain/ProjectSync/getLocalChanges.tests.ts @@ -0,0 +1,146 @@ +import { fc } from '@code-expert/test-utils'; +import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; +import { Lens } from 'monocle-ts'; +import * as nodePath from 'path'; +import { assert, describe, it } from 'vitest'; +import { nonEmptyArray, option, pipe, task, taskEither } from '@code-expert/prelude'; +import { + LocalFileChange, + LocalFileInfo, + getLocalChanges, + localChangeType, +} from '@/domain/FileSystem'; +import { pfsPathArb } from '@/domain/FileSystem/Path.tests'; +import { FileSystemStack } from '@/domain/FileSystem/fileSystemStack'; +import { ordFsNode } from '@/lib/tauri/fs'; +import { escape } from '@/lib/tauri/path'; +import { panic } from '@/utils/error'; + +const Hash = Lens.fromProp()('hash'); +const modifyHash = Hash.modify((h) => h + '2'); + +export const nodeFsStack: FileSystemStack = { + dirname: (path) => taskEither.fromIO(() => nodePath.dirname(path)), + join: (...paths) => task.fromIO(() => nodePath.join(...paths)), + stripAncestor: (ancestor) => (to) => + taskEither.fromIO(() => { + const relative = nodePath.relative(ancestor, to); + const normalized = nodePath.normalize(relative); + return nodePath.format({ dir: '.', base: normalized }); + }), + escape, + getFileHash: () => panic('getFileHash is not implemented on nodeFsStack'), + removeFile: () => panic('removeFile is not implemented on nodeFsStack'), +}; + +const localFileInfoArb = fc.record({ + type: fc.constant('file'), + path: pfsPathArb, + hash: fc.string({ minLength: 8, maxLength: 8 }), +}); + +describe('getLocalChanges', () => { + const toRemoved = (fileInfo: LocalFileInfo): LocalFileChange => ({ + path: fileInfo.path, + type: 'file', + change: localChangeType.removed(), + }); + const toAdded = (fileInfo: LocalFileInfo): LocalFileChange => ({ + path: fileInfo.path, + type: 'file', + change: localChangeType.added(), + }); + const toUpdated = (fileInfo: LocalFileInfo): LocalFileChange => ({ + path: fileInfo.path, + type: 'file', + change: localChangeType.updated(), + }); + + it('should classify missing files as "removed"', () => { + const commonFilesArb = fc.array(localFileInfoArb); + const missingFilesArb = fc.nonEmptyArray(localFileInfoArb); + + const missingToRemoved = ( + commonFiles: Array, + missingFiles: NonEmptyArray, + ) => + assert.deepEqual( + getLocalChanges([...commonFiles, ...missingFiles], [...commonFiles]), + pipe(missingFiles, nonEmptyArray.map(toRemoved), option.some), + ); + + fc.assert(fc.property(commonFilesArb, missingFilesArb, missingToRemoved)); + }); + + it('should classify new files as "added"', () => { + const commonFilesArb = fc.array(localFileInfoArb); + const newFilesArb = fc.nonEmptyArray(localFileInfoArb); + + const newToAdded = ( + commonFiles: Array, + newFiles: NonEmptyArray, + ) => + assert.deepEqual( + getLocalChanges([...commonFiles], [...commonFiles, ...newFiles]), + pipe(newFiles, nonEmptyArray.map(toAdded), option.some), + ); + + fc.assert(fc.property(commonFilesArb, newFilesArb, newToAdded)); + }); + + it('should classify files with a different version as "updated"', () => { + const commonFilesArb = fc.array(localFileInfoArb); + const changedFilesArb = fc.nonEmptyArray(localFileInfoArb); + + const changedToUpdated = ( + commonFiles: Array, + changedFiles: NonEmptyArray, + ) => + assert.deepEqual( + getLocalChanges( + [...commonFiles, ...changedFiles], + [...commonFiles, ...changedFiles.map(modifyHash)], + ), + pipe(changedFiles, nonEmptyArray.map(toUpdated), option.some), + ); + + fc.assert(fc.property(commonFilesArb, changedFilesArb, changedToUpdated)); + }); + + it('should do all three at the same time', () => { + const commonFilesArb = fc.array(localFileInfoArb); + const missingFilesArb = fc.nonEmptyArray(localFileInfoArb); + const newFilesArb = fc.nonEmptyArray(localFileInfoArb); + const changedFilesArb = fc.nonEmptyArray(localFileInfoArb); + + const removedNewChanged = ( + commonFiles: Array, + missingFiles: NonEmptyArray, + newFiles: NonEmptyArray, + changedFiles: NonEmptyArray, + ) => + assert.deepEqual( + pipe( + getLocalChanges( + [...commonFiles, ...missingFiles, ...changedFiles], + [...commonFiles, ...newFiles, ...changedFiles.map(modifyHash)], + ), + option.map(nonEmptyArray.sort(ordFsNode)), + ), + pipe( + [ + nonEmptyArray.map(toRemoved)(missingFiles), + nonEmptyArray.map(toAdded)(newFiles), + nonEmptyArray.map(toUpdated)(changedFiles), + ], + nonEmptyArray.concatAll(nonEmptyArray.getSemigroup()), + nonEmptyArray.sort(ordFsNode), + option.some, + ), + ); + + fc.assert( + fc.property(commonFilesArb, missingFilesArb, newFilesArb, changedFilesArb, removedNewChanged), + ); + }); +}); From 91f0f217d6b4557aa2dbac0198033a2c7415e582 Mon Sep 17 00:00:00 2001 From: Samuel Vogelsanger Date: Wed, 9 Aug 2023 12:42:58 +0200 Subject: [PATCH 5/6] refact: use DI consistently in useProjectSync.ts --- src/domain/FileSystem/HashInfo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/FileSystem/HashInfo.ts b/src/domain/FileSystem/HashInfo.ts index 27816c9a..63aadf55 100644 --- a/src/domain/FileSystem/HashInfo.ts +++ b/src/domain/FileSystem/HashInfo.ts @@ -15,6 +15,7 @@ export const hashInfoFromFsFile = pipe( stack.join(projectDir, file.path), task.chain(stack.getFileHash), + // fixme: should we use panic here? taskEither.getOrElse((e) => { throw e; }), From 704d6cd759c8476ce175a5249cf29ee4ccd99723 Mon Sep 17 00:00:00 2001 From: Samuel Vogelsanger Date: Wed, 9 Aug 2023 19:58:00 +0200 Subject: [PATCH 6/6] fix: mock implementation --- .../ProjectSync/getLocalChanges.tests.ts | 6 +++++- .../ProjectSync/getRemoteChanges.tests.ts | 20 +------------------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/domain/ProjectSync/getLocalChanges.tests.ts b/src/domain/ProjectSync/getLocalChanges.tests.ts index e27a6902..c264ec9b 100644 --- a/src/domain/ProjectSync/getLocalChanges.tests.ts +++ b/src/domain/ProjectSync/getLocalChanges.tests.ts @@ -3,7 +3,7 @@ import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; import { Lens } from 'monocle-ts'; import * as nodePath from 'path'; import { assert, describe, it } from 'vitest'; -import { nonEmptyArray, option, pipe, task, taskEither } from '@code-expert/prelude'; +import { nonEmptyArray, option, pipe, task, taskEither, taskOption } from '@code-expert/prelude'; import { LocalFileChange, LocalFileInfo, @@ -31,6 +31,10 @@ export const nodeFsStack: FileSystemStack = { escape, getFileHash: () => panic('getFileHash is not implemented on nodeFsStack'), removeFile: () => panic('removeFile is not implemented on nodeFsStack'), + basename: (path) => pipe(path.split('/'), (ps) => ps[ps.length - 1], taskOption.of), + tempDir: () => panic('tempDir is not implemented on nodeFsStack'), + readBinaryFile: () => panic('readBinaryFile is not implemented on nodeFsStack'), + readFsTree: () => panic('readFsTree is not implemented on nodeFsStack'), }; const localFileInfoArb = fc.record({ diff --git a/src/domain/ProjectSync/getRemoteChanges.tests.ts b/src/domain/ProjectSync/getRemoteChanges.tests.ts index 0ed640a1..c52f74cf 100644 --- a/src/domain/ProjectSync/getRemoteChanges.tests.ts +++ b/src/domain/ProjectSync/getRemoteChanges.tests.ts @@ -1,9 +1,8 @@ import { fc } from '@code-expert/test-utils'; import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; import { Lens } from 'monocle-ts'; -import * as nodePath from 'path'; import { assert, describe, it } from 'vitest'; -import { nonEmptyArray, option, pipe, task, taskEither } from '@code-expert/prelude'; +import { nonEmptyArray, option, pipe } from '@code-expert/prelude'; import { RemoteDirInfo, RemoteFileChange, @@ -13,28 +12,11 @@ import { remoteChangeType, } from '@/domain/FileSystem'; import { pfsPathArb } from '@/domain/FileSystem/Path.tests'; -import { FileSystemStack } from '@/domain/FileSystem/fileSystemStack'; import { ordFsNode } from '@/lib/tauri/fs'; -import { escape } from '@/lib/tauri/path'; -import { panic } from '@/utils/error'; const Version = Lens.fromProp()('version'); const incrementVersion = Version.modify((v) => v + 1); -export const nodeFsStack: FileSystemStack = { - dirname: (path) => taskEither.fromIO(() => nodePath.dirname(path)), - join: (...paths) => task.fromIO(() => nodePath.join(...paths)), - stripAncestor: (ancestor) => (to) => - taskEither.fromIO(() => { - const relative = nodePath.relative(ancestor, to); - const normalized = nodePath.normalize(relative); - return nodePath.format({ dir: '.', base: normalized }); - }), - escape, - getFileHash: () => panic('getFileHash is not implemented on nodeFsStack'), - removeFile: () => panic('removeFile is not implemented on nodeFsStack'), -}; - const remoteFileInfoArb = fc.record({ type: fc.constant('file'), path: pfsPathArb,