diff --git a/CliClient/run_test.sh b/CliClient/run_test.sh index 4af0b35e0..17794306e 100755 --- a/CliClient/run_test.sh +++ b/CliClient/run_test.sh @@ -10,7 +10,7 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/" mkdir -p "$BUILD_DIR/data" if [[ $TEST_FILE != "" ]]; then - (cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js) + (cd "$ROOT_DIR" && NODE_ENV=testing npm test tests-build/$TEST_FILE.js) exit fi @@ -21,27 +21,27 @@ function finish { trap finish EXIT cd "$ROOT_DIR" -npm test tests-build/ArrayUtils.js && \ -npm test tests-build/encryption.js && \ -npm test tests-build/EnexToMd.js && \ -npm test tests-build/HtmlToMd.js && \ -npm test tests-build/markdownUtils.js && \ -npm test tests-build/models_BaseItem.js && \ -npm test tests-build/models_Folder.js && \ -npm test tests-build/models_ItemChange.js && \ -npm test tests-build/models_Note.js && \ -npm test tests-build/models_Resource.js && \ -npm test tests-build/models_Revision.js && \ -npm test tests-build/models_Setting.js && \ -npm test tests-build/models_Tag.js && \ -npm test tests-build/pathUtils.js && \ -npm test tests-build/services_InteropService.js && \ -npm test tests-build/services_KvStore.js && \ -npm test tests-build/services_ResourceService.js && \ -npm test tests-build/services_rest_Api.js && \ -npm test tests-build/services_SearchEngine.js && \ -npm test tests-build/services_Revision.js && \ -npm test tests-build/StringUtils.js && \ -npm test tests-build/TaskQueue.js && \ -npm test tests-build/synchronizer.js && \ -npm test tests-build/urlUtils.js \ No newline at end of file +NODE_ENV=testing npm test tests-build/ArrayUtils.js && \ +NODE_ENV=testing npm test tests-build/encryption.js && \ +NODE_ENV=testing npm test tests-build/EnexToMd.js && \ +NODE_ENV=testing npm test tests-build/HtmlToMd.js && \ +NODE_ENV=testing npm test tests-build/markdownUtils.js && \ +NODE_ENV=testing npm test tests-build/models_BaseItem.js && \ +NODE_ENV=testing npm test tests-build/models_Folder.js && \ +NODE_ENV=testing npm test tests-build/models_ItemChange.js && \ +NODE_ENV=testing npm test tests-build/models_Note.js && \ +NODE_ENV=testing npm test tests-build/models_Resource.js && \ +NODE_ENV=testing npm test tests-build/models_Revision.js && \ +NODE_ENV=testing npm test tests-build/models_Setting.js && \ +NODE_ENV=testing npm test tests-build/models_Tag.js && \ +NODE_ENV=testing npm test tests-build/pathUtils.js && \ +NODE_ENV=testing npm test tests-build/services_InteropService.js && \ +NODE_ENV=testing npm test tests-build/services_KvStore.js && \ +NODE_ENV=testing npm test tests-build/services_ResourceService.js && \ +NODE_ENV=testing npm test tests-build/services_rest_Api.js && \ +NODE_ENV=testing npm test tests-build/services_SearchEngine.js && \ +NODE_ENV=testing npm test tests-build/services_Revision.js && \ +NODE_ENV=testing npm test tests-build/StringUtils.js && \ +NODE_ENV=testing npm test tests-build/TaskQueue.js && \ +NODE_ENV=testing npm test tests-build/synchronizer.js && \ +NODE_ENV=testing npm test tests-build/urlUtils.js \ No newline at end of file diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index 3758b8029..707208d78 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -1481,19 +1481,27 @@ describe('Synchronizer', function() { })); it('should not wipe out user data when syncing with an empty target', asyncTest(async () => { - await Note.save({ title: 'ma note' }); - await Note.save({ title: 'mon autre note' }); - await Note.save({ title: 'ma troisième note' }); + for (let i = 0; i < 10; i++) await Note.save({ title: 'note' }); Setting.setValue('sync.wipeOutFailSafe', true); await synchronizer().start(); await fileApi().clearRoot(); // oops await synchronizer().start(); - expect((await Note.all()).length).toBe(3); // but since the fail-safe if on, the notes have not been deleted + expect((await Note.all()).length).toBe(10); // but since the fail-safe if on, the notes have not been deleted Setting.setValue('sync.wipeOutFailSafe', false); // Now switch it off await synchronizer().start(); expect((await Note.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared + + // Handle case where the sync target has been wiped out, then the user creates one note and sync. + + for (let i = 0; i < 10; i++) await Note.save({ title: 'note' }); + Setting.setValue('sync.wipeOutFailSafe', true); + await synchronizer().start(); + await fileApi().clearRoot(); + await Note.save({ title: 'ma note encore' }); + await synchronizer().start(); + expect((await Note.all()).length).toBe(11); })); }); diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index 52d9ac51e..34a83821e 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -5,6 +5,7 @@ const BaseItem = require('lib/models/BaseItem.js'); const JoplinError = require('lib/JoplinError'); const ArrayUtils = require('lib/ArrayUtils'); const { time } = require('lib/time-utils.js'); +const { sprintf } = require('sprintf-js'); function requestCanBeRepeated(error) { const errorCode = typeof error === 'object' && error.code ? error.code : null; @@ -262,13 +263,6 @@ async function basicDelta(path, getDirStatFn, options) { }); newContext.statIdsCache = newContext.statsCache.filter(item => BaseItem.isSystemPath(item.path)).map(item => BaseItem.pathToId(item.path)); newContext.statIdsCache.sort(); // Items must be sorted to use binary search below - - // At this point statIdsCache contains the list of all the item IDs on the sync target. - // If it's empty, it's most likely a configuration error or bug. For example, if the - // user moves their Nextcloud directory, or if a network drive gets disconnected and - // returns an empty dir instead of an error. In that case, we don't wipe out the user - // data, unless they have switched off the fail-safe. - if (options.wipeOutFailSafe && !newContext.statIdsCache.length) throw new JoplinError('Fail-safe: The delta operation was interrupted because the sync target appears to be empty. To override this behaviour disable the "Wipe out fail-safe" option in the settings.', 'failSafe'); } let output = []; @@ -322,6 +316,15 @@ async function basicDelta(path, getDirStatFn, options) { } } + const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0; + + // If more than 90% of the notes are going to be deleted, it's most likely a + // configuration error or bug. For example, if the user moves their Nextcloud + // directory, or if a network drive gets disconnected and returns an empty dir + // instead of an error. In that case, we don't wipe out the user data, unless + // they have switched off the fail-safe. + if (options.wipeOutFailSafe && percentDeleted >= 0.90) throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe'); + output = output.concat(deletedItems); } diff --git a/ReactNativeClient/lib/shim-init-node.js b/ReactNativeClient/lib/shim-init-node.js index de268fb20..87f040ad6 100644 --- a/ReactNativeClient/lib/shim-init-node.js +++ b/ReactNativeClient/lib/shim-init-node.js @@ -362,6 +362,10 @@ function shimInit() { bridge().openExternal(url); }; + shim.isTestingEnv = () => { + return process.env.NODE_ENV === 'testing'; + }; + shim.waitForFrame = () => {}; } diff --git a/ReactNativeClient/lib/shim.js b/ReactNativeClient/lib/shim.js index 8cde2e3d5..f39bcb31e 100644 --- a/ReactNativeClient/lib/shim.js +++ b/ReactNativeClient/lib/shim.js @@ -198,4 +198,8 @@ shim.loadCssFromJs = name => { throw new Error('Not implemented'); }; +shim.isTestingEnv = () => { + return false; +}; + module.exports = { shim }; diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index 1b4bd9c6e..2dde431ad 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -715,7 +715,11 @@ class Synchronizer { // Or it's a temporary issue that will be resolved on next sync. this.logger().info(error.message); - if (error.code === 'failSafe') this.logLastRequests(); + if (error.code === 'failSafe') { + // Get the message to display on UI, but not in testing to avoid poluting stdout + if (!shim.isTestingEnv()) this.progressReport_.errors.push(error.message); + this.logLastRequests(); + } } else if (error.code === 'unknownItemType') { this.progressReport_.errors.push(_('Unknown item type downloaded - please upgrade Joplin to the latest version')); this.logger().error(error);