Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
68
scripts/test/logseq/check-ios-web-fonts.test.mjs
Normal file
68
scripts/test/logseq/check-ios-web-fonts.test.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
findUnsupportedIOSFontAssets,
|
||||
} from '../../lib/check-ios-web-fonts.mjs'
|
||||
|
||||
test('findUnsupportedIOSFontAssets rejects woff and woff2 files and CSS references', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logseq-ios-fonts-'))
|
||||
const assetRoot = path.join(tempDir, 'public')
|
||||
const fontDir = path.join(assetRoot, 'css', 'fonts')
|
||||
fs.mkdirSync(fontDir, { recursive: true })
|
||||
|
||||
fs.writeFileSync(path.join(fontDir, 'KaTeX_Main-Regular.ttf'), '')
|
||||
fs.writeFileSync(path.join(fontDir, 'KaTeX_Main-Regular.woff'), '')
|
||||
fs.writeFileSync(path.join(fontDir, 'KaTeX_Main-Regular.woff2'), '')
|
||||
fs.writeFileSync(
|
||||
path.join(assetRoot, 'css', 'style.css'),
|
||||
[
|
||||
'@font-face{src:url(fonts/KaTeX_Main-Regular.woff) format("woff")}',
|
||||
'@font-face{src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2")}',
|
||||
'@font-face{src:url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}',
|
||||
].join('\n'))
|
||||
|
||||
try {
|
||||
const result = findUnsupportedIOSFontAssets({
|
||||
repoRoot: tempDir,
|
||||
assetRoots: [assetRoot],
|
||||
})
|
||||
|
||||
assert.deepEqual(result.fontFiles, [
|
||||
'public/css/fonts/KaTeX_Main-Regular.woff',
|
||||
'public/css/fonts/KaTeX_Main-Regular.woff2',
|
||||
])
|
||||
assert.deepEqual(result.cssFiles, [
|
||||
'public/css/style.css',
|
||||
])
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('findUnsupportedIOSFontAssets accepts ttf-only assets', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logseq-ios-fonts-'))
|
||||
const assetRoot = path.join(tempDir, 'public')
|
||||
const fontDir = path.join(assetRoot, 'css', 'fonts')
|
||||
fs.mkdirSync(fontDir, { recursive: true })
|
||||
|
||||
fs.writeFileSync(path.join(fontDir, 'KaTeX_Main-Regular.ttf'), '')
|
||||
fs.writeFileSync(
|
||||
path.join(assetRoot, 'css', 'style.css'),
|
||||
'@font-face{src:url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}')
|
||||
|
||||
try {
|
||||
const result = findUnsupportedIOSFontAssets({
|
||||
repoRoot: tempDir,
|
||||
assetRoots: [assetRoot],
|
||||
})
|
||||
|
||||
assert.deepEqual(result.fontFiles, [])
|
||||
assert.deepEqual(result.cssFiles, [])
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
13
scripts/test/logseq/gulpfile-tasks.test.mjs
Normal file
13
scripts/test/logseq/gulpfile-tasks.test.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
import test from 'node:test'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
test('gulpfile registers app watch tasks', () => {
|
||||
const gulpfile = require('../../../gulpfile.js')
|
||||
|
||||
assert.equal(typeof gulpfile.watch, 'function')
|
||||
assert.equal(typeof gulpfile.watchMobile, 'function')
|
||||
assert.equal(typeof gulpfile.build, 'function')
|
||||
})
|
||||
84
scripts/test/logseq/logseq-electron-op-sim.test.cjs
Normal file
84
scripts/test/logseq/logseq-electron-op-sim.test.cjs
Normal file
@@ -0,0 +1,84 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
buildOperationPlan,
|
||||
chooseRunnableOperation,
|
||||
parseArgs,
|
||||
} = require('../../lib/logseq-electron-op-sim.cjs');
|
||||
|
||||
test('buildOperationPlan generates requested length and all operation kinds', () => {
|
||||
const plan = buildOperationPlan(500);
|
||||
|
||||
assert.equal(plan.length, 500);
|
||||
assert.deepEqual(plan.slice(0, 9), [
|
||||
'add',
|
||||
'copyPaste',
|
||||
'copyPasteTreeToEmptyTarget',
|
||||
'move',
|
||||
'indent',
|
||||
'outdent',
|
||||
'delete',
|
||||
'undo',
|
||||
'redo',
|
||||
]);
|
||||
|
||||
const kinds = new Set(plan);
|
||||
assert.deepEqual(
|
||||
kinds,
|
||||
new Set([
|
||||
'add',
|
||||
'copyPaste',
|
||||
'copyPasteTreeToEmptyTarget',
|
||||
'move',
|
||||
'indent',
|
||||
'outdent',
|
||||
'delete',
|
||||
'undo',
|
||||
'redo',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('buildOperationPlan rejects invalid total operation count', () => {
|
||||
assert.throws(() => buildOperationPlan(0), /positive integer/);
|
||||
assert.throws(() => buildOperationPlan(-1), /positive integer/);
|
||||
assert.throws(() => buildOperationPlan(12.5), /positive integer/);
|
||||
assert.throws(() => buildOperationPlan('500'), /positive integer/);
|
||||
});
|
||||
|
||||
test('chooseRunnableOperation falls back to add when operation preconditions are unmet', () => {
|
||||
assert.equal(chooseRunnableOperation('copyPaste', 0), 'add');
|
||||
assert.equal(chooseRunnableOperation('copyPasteTreeToEmptyTarget', 0), 'add');
|
||||
assert.equal(chooseRunnableOperation('move', 1), 'add');
|
||||
assert.equal(chooseRunnableOperation('indent', 1), 'add');
|
||||
assert.equal(chooseRunnableOperation('delete', 1), 'add');
|
||||
|
||||
assert.equal(chooseRunnableOperation('move', 2), 'move');
|
||||
assert.equal(chooseRunnableOperation('indent', 2), 'indent');
|
||||
assert.equal(chooseRunnableOperation('outdent', 1), 'outdent');
|
||||
assert.equal(chooseRunnableOperation('delete', 2), 'delete');
|
||||
assert.equal(chooseRunnableOperation('undo', 0), 'undo');
|
||||
assert.equal(chooseRunnableOperation('redo', 0), 'redo');
|
||||
});
|
||||
|
||||
test('parseArgs defaults to 500 ops on default debug port', () => {
|
||||
const args = parseArgs([]);
|
||||
assert.equal(args.ops, 500);
|
||||
assert.equal(args.port, 9333);
|
||||
assert.equal(args.undoRedoDelayMs, 350);
|
||||
});
|
||||
|
||||
test('parseArgs supports overrides', () => {
|
||||
const args = parseArgs(['--ops', '750', '--port', '9444', '--undo-redo-delay-ms', '200']);
|
||||
assert.equal(args.ops, 750);
|
||||
assert.equal(args.port, 9444);
|
||||
assert.equal(args.undoRedoDelayMs, 200);
|
||||
});
|
||||
|
||||
test('parseArgs rejects invalid numbers', () => {
|
||||
assert.throws(() => parseArgs(['--ops', '0']), /--ops must be a positive integer/);
|
||||
assert.throws(() => parseArgs(['--ops', '499']), /--ops must be at least 500/);
|
||||
assert.throws(() => parseArgs(['--port', '-1']), /--port must be a positive integer/);
|
||||
assert.throws(() => parseArgs(['--undo-redo-delay-ms', '-1']), /--undo-redo-delay-ms must be a non-negative integer/);
|
||||
});
|
||||
446
scripts/test/logseq/sync-open-chrome-tab-simulate.test.cjs
Normal file
446
scripts/test/logseq/sync-open-chrome-tab-simulate.test.cjs
Normal file
@@ -0,0 +1,446 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
parseArgs,
|
||||
isRetryableAgentBrowserError,
|
||||
buildCleanupTodayPageProgram,
|
||||
classifySimulationFailure,
|
||||
buildRejectedResultEntry,
|
||||
extractChecksumMismatchDetailsFromError,
|
||||
extractTxRejectedDetailsFromError,
|
||||
buildRunArtifact,
|
||||
createSeededRng,
|
||||
shuffleOperationPlan,
|
||||
extractReplayContext,
|
||||
buildSimulationOperationPlan,
|
||||
mergeOutlinerCoverageIntoRound,
|
||||
ALL_OUTLINER_OP_COVERAGE_OPS,
|
||||
} = require('../../sync-open-chrome-tab-simulate.cjs');
|
||||
|
||||
const OUTLINER_OP_SCHEMA_PATH = path.resolve(
|
||||
__dirname,
|
||||
'../../../deps/outliner/src/logseq/outliner/op.cljs'
|
||||
);
|
||||
const OUTLINER_OP_CONSTRUCT_PATH = path.resolve(
|
||||
__dirname,
|
||||
'../../../deps/outliner/src/logseq/outliner/op/construct.cljc'
|
||||
);
|
||||
|
||||
function extractSection(sourceText, startToken, endToken) {
|
||||
const start = sourceText.indexOf(startToken);
|
||||
if (start < 0) {
|
||||
throw new Error(`Missing start token: ${startToken}`);
|
||||
}
|
||||
const end = sourceText.indexOf(endToken, start);
|
||||
if (end < 0) {
|
||||
throw new Error(`Missing end token: ${endToken}`);
|
||||
}
|
||||
return sourceText.slice(start, end);
|
||||
}
|
||||
|
||||
function parseOpSchemaOps(sourceText) {
|
||||
const section = extractSection(
|
||||
sourceText,
|
||||
'(def ^:private ^:large-vars/data-var op-schema',
|
||||
'(def ^:private ops-schema'
|
||||
);
|
||||
const ops = new Set();
|
||||
for (const match of section.matchAll(/\[\:([a-z0-9-]+)\s*\n\s+\[:catn/g)) {
|
||||
ops.add(match[1]);
|
||||
}
|
||||
return [...ops];
|
||||
}
|
||||
|
||||
function parseSemanticOps(sourceText) {
|
||||
const section = extractSection(
|
||||
sourceText,
|
||||
'(def ^:api semantic-outliner-ops',
|
||||
'(def ^:private transient-block-keys'
|
||||
);
|
||||
const setStart = section.indexOf('#{');
|
||||
const setEnd = section.indexOf('}', setStart);
|
||||
if (setStart < 0 || setEnd < 0 || setEnd <= setStart) {
|
||||
throw new Error('Failed to parse semantic-outliner-ops set');
|
||||
}
|
||||
const setText = section.slice(setStart + 2, setEnd);
|
||||
const ops = new Set();
|
||||
for (const match of setText.matchAll(/:([a-z0-9-]+)/g)) {
|
||||
ops.add(match[1]);
|
||||
}
|
||||
return [...ops];
|
||||
}
|
||||
|
||||
test('isRetryableAgentBrowserError treats transient CDP navigation closures as retryable', () => {
|
||||
const navigationClosed = new Error(
|
||||
'CDP error (Runtime.evaluate): Inspected target navigated or closed'
|
||||
);
|
||||
const contextDestroyed = new Error(
|
||||
'CDP error (Runtime.evaluate): Execution context was destroyed.'
|
||||
);
|
||||
const responseChannelClosed = new Error('CDP response channel closed');
|
||||
const pageStillLoading = new Error(
|
||||
'Operation timed out. The page may still be loading or the element may not exist.'
|
||||
);
|
||||
|
||||
assert.equal(isRetryableAgentBrowserError(navigationClosed), true);
|
||||
assert.equal(isRetryableAgentBrowserError(contextDestroyed), true);
|
||||
assert.equal(isRetryableAgentBrowserError(responseChannelClosed), true);
|
||||
assert.equal(isRetryableAgentBrowserError(pageStillLoading), true);
|
||||
assert.equal(
|
||||
isRetryableAgentBrowserError(new Error('Evaluation error: Failed to create client root block')),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('parseArgs defaults keep today-journal cleanup enabled', () => {
|
||||
const args = parseArgs([]);
|
||||
assert.equal(args.simulationPage, undefined);
|
||||
assert.equal(args.opProfile, 'fast');
|
||||
assert.equal(args.scenario, 'online');
|
||||
assert.equal(args.opTimeoutMs, 1000);
|
||||
assert.equal(args.cleanupTodayPage, true);
|
||||
});
|
||||
|
||||
test('parseArgs accepts offline simulation scenario', () => {
|
||||
const args = parseArgs(['--scenario', 'offline']);
|
||||
assert.equal(args.scenario, 'offline');
|
||||
});
|
||||
|
||||
test('parseArgs rejects unsupported simulation scenario', () => {
|
||||
assert.throws(
|
||||
() => parseArgs(['--scenario', 'invalid-mode']),
|
||||
/--scenario must be one of: online, offline/
|
||||
);
|
||||
});
|
||||
|
||||
test('cleanup program targets today journal APIs', () => {
|
||||
const program = buildCleanupTodayPageProgram();
|
||||
assert.match(program, /get_today_page/);
|
||||
assert.doesNotMatch(program, /testing journal/i);
|
||||
});
|
||||
|
||||
test('classifySimulationFailure detects checksum mismatch failures', () => {
|
||||
const mismatchError = new Error(
|
||||
'Evaluation error: Error: checksum mismatch rtc-log detected: {"type":":rtc.log/checksum-mismatch"}'
|
||||
);
|
||||
const timeoutError = new Error('Operation timed out. The page may still be loading');
|
||||
|
||||
assert.equal(classifySimulationFailure(mismatchError), 'checksum_mismatch');
|
||||
assert.equal(classifySimulationFailure(timeoutError), 'other');
|
||||
});
|
||||
|
||||
test('classifySimulationFailure detects tx-rejected failures', () => {
|
||||
const txRejectedError = new Error(
|
||||
'Evaluation error: Error: tx rejected rtc-log detected: {"type":":rtc.log/tx-rejected","reason":"db transact failed"}'
|
||||
);
|
||||
|
||||
assert.equal(classifySimulationFailure(txRejectedError), 'tx_rejected');
|
||||
});
|
||||
|
||||
test('classifySimulationFailure treats opfs access-handle lock errors as other', () => {
|
||||
const opfsLockError = new Error(
|
||||
"NoModificationAllowedError: Failed to execute 'createSyncAccessHandle' on 'FileSystemFileHandle'"
|
||||
);
|
||||
|
||||
assert.equal(classifySimulationFailure(opfsLockError), 'other');
|
||||
});
|
||||
|
||||
test('buildRejectedResultEntry marks peer as cancelled after checksum mismatch fail-fast', () => {
|
||||
const failFastState = {
|
||||
sourceIndex: 0,
|
||||
reasonType: 'checksum_mismatch',
|
||||
};
|
||||
|
||||
const peer = buildRejectedResultEntry(
|
||||
'logseq-op-sim-2',
|
||||
1,
|
||||
new Error('Command timed out'),
|
||||
failFastState
|
||||
);
|
||||
assert.equal(peer.cancelled, true);
|
||||
assert.equal(peer.cancelledReason, 'cancelled_due_to_peer_checksum_mismatch');
|
||||
assert.equal(peer.peerInstanceIndex, 1);
|
||||
|
||||
const source = buildRejectedResultEntry(
|
||||
'logseq-op-sim-1',
|
||||
0,
|
||||
new Error('checksum mismatch rtc-log detected'),
|
||||
failFastState
|
||||
);
|
||||
assert.equal(source.cancelled, undefined);
|
||||
assert.equal(source.failureType, 'checksum_mismatch');
|
||||
});
|
||||
|
||||
test('buildRejectedResultEntry marks peer as cancelled after tx-rejected fail-fast', () => {
|
||||
const failFastState = {
|
||||
sourceIndex: 1,
|
||||
reasonType: 'tx_rejected',
|
||||
};
|
||||
|
||||
const peer = buildRejectedResultEntry(
|
||||
'logseq-op-sim-1',
|
||||
0,
|
||||
new Error('Command timed out'),
|
||||
failFastState
|
||||
);
|
||||
assert.equal(peer.cancelled, true);
|
||||
assert.equal(peer.cancelledReason, 'cancelled_due_to_peer_tx_rejected');
|
||||
assert.equal(peer.peerInstanceIndex, 2);
|
||||
|
||||
const source = buildRejectedResultEntry(
|
||||
'logseq-op-sim-2',
|
||||
1,
|
||||
new Error('tx rejected rtc-log detected'),
|
||||
failFastState
|
||||
);
|
||||
assert.equal(source.cancelled, undefined);
|
||||
assert.equal(source.failureType, 'tx_rejected');
|
||||
});
|
||||
|
||||
test('buildRejectedResultEntry does not cancel peer on opfs lock fail-fast reason', () => {
|
||||
const failFastState = {
|
||||
sourceIndex: 0,
|
||||
reasonType: 'opfs_access_handle_lock',
|
||||
};
|
||||
|
||||
const peer = buildRejectedResultEntry(
|
||||
'logseq-op-sim-2',
|
||||
1,
|
||||
new Error('Command timed out'),
|
||||
failFastState
|
||||
);
|
||||
assert.equal(peer.cancelled, undefined);
|
||||
assert.equal(peer.failureType, 'other');
|
||||
});
|
||||
|
||||
test('extractChecksumMismatchDetailsFromError parses rtc-log payload JSON', () => {
|
||||
const errorText =
|
||||
'Evaluation error: Error: checksum mismatch rtc-log detected: {"type":":rtc.log/checksum-mismatch","messageType":"tx/batch/ok","localTx":10,"remoteTx":10,"localChecksum":"aa","remoteChecksum":"bb"}';
|
||||
const parsed = extractChecksumMismatchDetailsFromError(errorText);
|
||||
assert.deepEqual(parsed, {
|
||||
type: ':rtc.log/checksum-mismatch',
|
||||
messageType: 'tx/batch/ok',
|
||||
localTx: 10,
|
||||
remoteTx: 10,
|
||||
localChecksum: 'aa',
|
||||
remoteChecksum: 'bb',
|
||||
});
|
||||
});
|
||||
|
||||
test('extractTxRejectedDetailsFromError parses rtc-log payload JSON', () => {
|
||||
const errorText =
|
||||
'Evaluation error: Error: tx rejected rtc-log detected: {"type":":rtc.log/tx-rejected","messageType":"tx/reject","reason":"db transact failed","remoteTx":3}';
|
||||
const parsed = extractTxRejectedDetailsFromError(errorText);
|
||||
assert.deepEqual(parsed, {
|
||||
type: ':rtc.log/tx-rejected',
|
||||
messageType: 'tx/reject',
|
||||
reason: 'db transact failed',
|
||||
remoteTx: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('buildRunArtifact includes mismatch diagnostics and per-client summaries', () => {
|
||||
const output = {
|
||||
ok: false,
|
||||
instances: 2,
|
||||
successCount: 0,
|
||||
failureCount: 2,
|
||||
results: [
|
||||
{
|
||||
session: 'logseq-op-sim-1',
|
||||
instanceIndex: 1,
|
||||
ok: false,
|
||||
error:
|
||||
'Evaluation error: Error: checksum mismatch rtc-log detected: {"type":":rtc.log/checksum-mismatch","localChecksum":"aa","remoteChecksum":"bb"}',
|
||||
},
|
||||
{
|
||||
session: 'logseq-op-sim-2',
|
||||
instanceIndex: 2,
|
||||
ok: false,
|
||||
cancelled: true,
|
||||
cancelledReason: 'cancelled_due_to_peer_tx_rejected',
|
||||
error:
|
||||
'Evaluation error: Error: tx rejected rtc-log detected: {"type":":rtc.log/tx-rejected","reason":"db transact failed","remoteTx":12}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const artifact = buildRunArtifact({
|
||||
output,
|
||||
args: { ops: 100, instances: 2 },
|
||||
runContext: { runId: 'run-1', runPrefix: 'op-sim-run-1-' },
|
||||
failFastState: { sourceIndex: 0, reasonType: 'checksum_mismatch' },
|
||||
});
|
||||
|
||||
assert.equal(artifact.runId, 'run-1');
|
||||
assert.equal(artifact.failFast.reasonType, 'checksum_mismatch');
|
||||
assert.equal(artifact.mismatchCount, 1);
|
||||
assert.equal(artifact.txRejectedCount, 1);
|
||||
assert.equal(artifact.clients.length, 2);
|
||||
assert.equal(artifact.clients[0].mismatch?.remoteChecksum, 'bb');
|
||||
assert.equal(artifact.clients[1].txRejected?.reason, 'db transact failed');
|
||||
});
|
||||
|
||||
test('parseArgs accepts seed for deterministic simulation', () => {
|
||||
const args = parseArgs(['--seed', 'abc123']);
|
||||
assert.equal(args.seed, 'abc123');
|
||||
});
|
||||
|
||||
test('createSeededRng produces deterministic sequence for same seed', () => {
|
||||
const a = createSeededRng('same-seed');
|
||||
const b = createSeededRng('same-seed');
|
||||
const seqA = [a(), a(), a(), a(), a()];
|
||||
const seqB = [b(), b(), b(), b(), b()];
|
||||
assert.deepEqual(seqA, seqB);
|
||||
});
|
||||
|
||||
test('shuffleOperationPlan is deterministic with seeded rng', () => {
|
||||
const plan = ['a', 'b', 'c', 'd', 'e', 'f'];
|
||||
const shuffledA = shuffleOperationPlan(plan, createSeededRng('seed-1'));
|
||||
const shuffledB = shuffleOperationPlan(plan, createSeededRng('seed-1'));
|
||||
const shuffledC = shuffleOperationPlan(plan, createSeededRng('seed-2'));
|
||||
|
||||
assert.deepEqual(shuffledA, shuffledB);
|
||||
assert.notDeepEqual(shuffledA, shuffledC);
|
||||
});
|
||||
|
||||
test('parseArgs accepts replay artifact path', () => {
|
||||
const args = parseArgs(['--replay', 'tmp/db-sync-repro/run-1/artifact.json']);
|
||||
assert.equal(args.replay, 'tmp/db-sync-repro/run-1/artifact.json');
|
||||
});
|
||||
|
||||
test('extractReplayContext returns args override and fixed client plans', () => {
|
||||
const artifact = {
|
||||
args: { ops: 100, instances: 2, seed: 'abc' },
|
||||
clients: [
|
||||
{ instanceIndex: 1, requestedPlan: ['add', 'move'] },
|
||||
{ instanceIndex: 2, requestedPlan: ['add', 'delete'] },
|
||||
],
|
||||
};
|
||||
const replay = extractReplayContext(artifact);
|
||||
assert.equal(replay.argsOverride.seed, 'abc');
|
||||
assert.deepEqual(replay.fixedPlansByInstance.get(1), ['add', 'move']);
|
||||
assert.deepEqual(replay.fixedPlansByInstance.get(2), ['add', 'delete']);
|
||||
});
|
||||
|
||||
test('buildSimulationOperationPlan full profile includes save, refs, templates, and multi-property ops', () => {
|
||||
const plan = buildSimulationOperationPlan(20, 'full');
|
||||
assert.deepEqual(plan, [
|
||||
'add',
|
||||
'save',
|
||||
'inlineTag',
|
||||
'emptyInlineTag',
|
||||
'pageReference',
|
||||
'blockReference',
|
||||
'propertySet',
|
||||
'batchSetProperty',
|
||||
'propertyValueDelete',
|
||||
'copyPaste',
|
||||
'copyPasteTreeToEmptyTarget',
|
||||
'templateApply',
|
||||
'move',
|
||||
'moveUpDown',
|
||||
'indent',
|
||||
'outdent',
|
||||
'delete',
|
||||
'propertyRemove',
|
||||
'undo',
|
||||
'redo',
|
||||
]);
|
||||
});
|
||||
|
||||
test('buildSimulationOperationPlan fast profile cycles through refs, templates, and property variants', () => {
|
||||
const plan = buildSimulationOperationPlan(17, 'fast');
|
||||
assert.deepEqual(plan, [
|
||||
'add',
|
||||
'save',
|
||||
'inlineTag',
|
||||
'emptyInlineTag',
|
||||
'pageReference',
|
||||
'blockReference',
|
||||
'propertySet',
|
||||
'batchSetProperty',
|
||||
'move',
|
||||
'delete',
|
||||
'indent',
|
||||
'outdent',
|
||||
'moveUpDown',
|
||||
'templateApply',
|
||||
'propertyValueDelete',
|
||||
'add',
|
||||
'move',
|
||||
]);
|
||||
});
|
||||
|
||||
test('ALL_OUTLINER_OP_COVERAGE_OPS tracks canonical outliner-op definitions', () => {
|
||||
const opSchemaSource = fs.readFileSync(OUTLINER_OP_SCHEMA_PATH, 'utf8');
|
||||
const opConstructSource = fs.readFileSync(OUTLINER_OP_CONSTRUCT_PATH, 'utf8');
|
||||
const expectedOps = [
|
||||
...new Set([
|
||||
...parseOpSchemaOps(opSchemaSource),
|
||||
...parseSemanticOps(opConstructSource),
|
||||
]),
|
||||
].sort();
|
||||
const actualOps = [...new Set(ALL_OUTLINER_OP_COVERAGE_OPS)].sort();
|
||||
|
||||
assert.equal(actualOps.length, ALL_OUTLINER_OP_COVERAGE_OPS.length);
|
||||
assert.deepEqual(actualOps, expectedOps);
|
||||
});
|
||||
|
||||
test('mergeOutlinerCoverageIntoRound prepends all outliner coverage ops to requested plan and op log', () => {
|
||||
const round = {
|
||||
requestedOps: 2,
|
||||
executedOps: 2,
|
||||
counts: { add: 1, move: 1 },
|
||||
requestedPlan: ['add', 'move'],
|
||||
opLog: [
|
||||
{ index: 0, requested: 'add', executedAs: 'add', detail: { kind: 'add' } },
|
||||
{ index: 1, requested: 'move', executedAs: 'move', detail: { kind: 'move' } },
|
||||
],
|
||||
outlinerOpCoverage: {
|
||||
expectedOps: ['save-block', 'set-block-property'],
|
||||
failedOps: [],
|
||||
sample: [
|
||||
{ op: 'save-block', ok: true, durationMs: 123 },
|
||||
{ op: 'set-block-property', ok: true, durationMs: 88 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const merged = mergeOutlinerCoverageIntoRound(round);
|
||||
assert.equal(merged.requestedOps, 4);
|
||||
assert.equal(merged.executedOps, 4);
|
||||
assert.deepEqual(merged.requestedPlan, [
|
||||
'outliner:save-block',
|
||||
'outliner:set-block-property',
|
||||
'add',
|
||||
'move',
|
||||
]);
|
||||
assert.equal(merged.opLog.length, 4);
|
||||
assert.equal(merged.opLog[0].requested, 'outliner:save-block');
|
||||
assert.equal(merged.opLog[1].requested, 'outliner:set-block-property');
|
||||
assert.equal(merged.opLog[2].index, 2);
|
||||
assert.equal(merged.opLog[3].index, 3);
|
||||
assert.equal(merged.counts.outlinerCoverage, 2);
|
||||
assert.equal(merged.counts.outlinerCoverageFailed, 0);
|
||||
});
|
||||
|
||||
test('mergeOutlinerCoverageIntoRound is idempotent once outliner ops are already merged', () => {
|
||||
const round = {
|
||||
requestedOps: 2,
|
||||
executedOps: 2,
|
||||
counts: {},
|
||||
requestedPlan: ['outliner:save-block', 'add'],
|
||||
opLog: [],
|
||||
outlinerOpCoverage: {
|
||||
expectedOps: ['save-block'],
|
||||
failedOps: [],
|
||||
},
|
||||
};
|
||||
|
||||
const merged = mergeOutlinerCoverageIntoRound(round);
|
||||
assert.deepEqual(merged, round);
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
(ns logseq.tasks.db-graph.create-graph-with-clojure-irc-history-test
|
||||
(:require ["fs" :as fs]
|
||||
["os" :as node-os]
|
||||
["path" :as node-path]
|
||||
[cljs.test :refer [deftest is testing]]
|
||||
[logseq.tasks.db-graph.create-graph-with-clojure-irc-history :as sut]))
|
||||
|
||||
(deftest extract-day-paths-returns-sorted-unique-day-pages
|
||||
(let [index-html (str "<a href=\"2008/2008-02-03.html\">3</a>"
|
||||
"<a href=\"2008/2008-02-01.html\">1</a>"
|
||||
"<a href=\"index.html\">home</a>"
|
||||
"<a href=\"2008/2008-02-01.html\">1</a>")]
|
||||
(is (= ["2008/2008-02-01.html" "2008/2008-02-03.html"]
|
||||
(sut/extract-day-paths index-html)))))
|
||||
|
||||
(deftest extract-chat-rows-parses-time-nick-and-message
|
||||
(let [day-html (str "<tr id=\"t1245A\"><td><a href=\"#t1245A\">12:45</a></td>"
|
||||
"<td class=\"n2\">otwieracz</td>"
|
||||
"<td>Hello "there" & <a href=\"https://example.com\">https://example.com</a></td></tr>"
|
||||
"<tr id=\"t1249A\" class=\"h\"><td><a href=\"#t1249A\">12:49</a></td>"
|
||||
"<td class=\"n6\">lxsameer</td>"
|
||||
"<td>otwieracz: selmer</td></tr>")]
|
||||
(is (= [{:time "12:45"
|
||||
:nick "otwieracz"
|
||||
:message "Hello \"there\" & https://example.com"}
|
||||
{:time "12:49"
|
||||
:nick "lxsameer"
|
||||
:message "otwieracz: selmer"}]
|
||||
(sut/extract-chat-rows day-html)))))
|
||||
|
||||
(deftest select-day-paths-downloads-all-by-default
|
||||
(let [paths ["2008/2008-02-01.html"
|
||||
"2008/2008-02-02.html"
|
||||
"2008/2008-02-03.html"
|
||||
"2008/2008-02-04.html"]]
|
||||
(is (= paths
|
||||
(sut/select-day-paths paths {})))
|
||||
(is (= ["2008/2008-02-01.html" "2008/2008-02-02.html"]
|
||||
(sut/select-day-paths paths {:limit-days 2})))))
|
||||
|
||||
(deftest build-page-and-blocks-uses-journal-page-and-chat-timestamps
|
||||
(testing "Each chat block stores created-by-ref and timestamp derived from date/time"
|
||||
(let [chouser-uuid #uuid "11111111-1111-1111-1111-111111111111"
|
||||
rhickey-uuid #uuid "22222222-2222-2222-2222-222222222222"
|
||||
page (sut/build-page-and-blocks {:date "2008-02-01"
|
||||
:day-path "2008/2008-02-01.html"
|
||||
:base-url "https://chouser.us/clojure-log/"
|
||||
:user->uuid {"Chouser" chouser-uuid
|
||||
"rhickey" rhickey-uuid}
|
||||
:chat-rows [{:time "20:46"
|
||||
:nick "Chouser"
|
||||
:message "Hi"}
|
||||
{:time "20:47"
|
||||
:nick "rhickey"
|
||||
:message "hello"}]})]
|
||||
(is (= 20080201
|
||||
(get-in page [:page :build/journal])))
|
||||
(is (= "20:46 Chouser: Hi"
|
||||
(get-in page [:blocks 0 :block/title])))
|
||||
(is (= "20:47 rhickey: hello"
|
||||
(get-in page [:blocks 1 :block/title])))
|
||||
(is (= [:block/uuid chouser-uuid]
|
||||
(get-in page [:blocks 0 :build/properties :logseq.property/created-by-ref])))
|
||||
(is (= [:block/uuid rhickey-uuid]
|
||||
(get-in page [:blocks 1 :build/properties :logseq.property/created-by-ref])))
|
||||
(is (= (sut/date-time->timestamp-ms "2008-02-01" "20:46")
|
||||
(get-in page [:blocks 0 :block/created-at])))
|
||||
(is (= (sut/date-time->timestamp-ms "2008-02-01" "20:47")
|
||||
(get-in page [:blocks 1 :block/created-at])))
|
||||
(is (= (sut/date-time->timestamp-ms "2008-02-01" "20:46")
|
||||
(get-in page [:blocks 0 :block/updated-at])))
|
||||
(is (= (sut/date-time->timestamp-ms "2008-02-01" "20:47")
|
||||
(get-in page [:blocks 1 :block/updated-at]))))))
|
||||
|
||||
(deftest build-user-pages-creates-user-page-entities
|
||||
(let [chouser-uuid #uuid "11111111-1111-1111-1111-111111111111"
|
||||
rhickey-uuid #uuid "22222222-2222-2222-2222-222222222222"
|
||||
pages (sut/build-user-pages {"Chouser" chouser-uuid
|
||||
"rhickey" rhickey-uuid})
|
||||
pages-by-title (into {} (map (fn [{:keys [page]}] [(:block/title page) page])) pages)]
|
||||
(is (= #{"Chouser" "rhickey"} (set (keys pages-by-title))))
|
||||
(is (= chouser-uuid (get-in pages-by-title ["Chouser" :block/uuid])))
|
||||
(is (= "Chouser" (get-in pages-by-title ["Chouser" :build/properties :logseq.property.user/name])))
|
||||
(is (= rhickey-uuid (get-in pages-by-title ["rhickey" :block/uuid])))
|
||||
(is (= "rhickey" (get-in pages-by-title ["rhickey" :build/properties :logseq.property.user/name])))))
|
||||
|
||||
(deftest build-page-and-blocks-handles-empty-chat-day
|
||||
(let [page (sut/build-page-and-blocks {:date "2008-02-01"
|
||||
:day-path "2008/2008-02-01.html"
|
||||
:base-url "https://chouser.us/clojure-log/"
|
||||
:user->uuid {}
|
||||
:chat-rows []})]
|
||||
(is (= 20080201
|
||||
(get-in page [:page :build/journal])))
|
||||
(is (= []
|
||||
(:blocks page)))))
|
||||
|
||||
(deftest resolve-source-dir-supports-root-and-direct-clojure-log-path
|
||||
(let [tmp-root (str (fs/mkdtempSync (node-path/join (node-os/tmpdir) "clj-log-test-")))
|
||||
repo-root (node-path/join tmp-root "chouser.github.io-master")
|
||||
clojure-log-dir (node-path/join repo-root "clojure-log")
|
||||
index-html "<html></html>"]
|
||||
(try
|
||||
(fs/mkdirSync clojure-log-dir #js {:recursive true})
|
||||
(fs/writeFileSync (node-path/join clojure-log-dir "index.html") index-html)
|
||||
(is (= clojure-log-dir
|
||||
(sut/resolve-source-dir clojure-log-dir)))
|
||||
(is (= clojure-log-dir
|
||||
(sut/resolve-source-dir repo-root)))
|
||||
(is (= clojure-log-dir
|
||||
(sut/resolve-source-dir tmp-root)))
|
||||
(finally
|
||||
(fs/rmSync tmp-root #js {:recursive true :force true})))))
|
||||
@@ -0,0 +1,69 @@
|
||||
(ns logseq.tasks.db-graph.create-graph-with-large-sizes-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[logseq.tasks.db-graph.create-graph-with-large-sizes :as sut]))
|
||||
|
||||
(deftest build-page-and-blocks-batch-builds-the-requested-graph-slice
|
||||
(let [id-seq (map #(str "id-" %) (range))
|
||||
next-id (let [ids (atom id-seq)]
|
||||
(fn []
|
||||
(let [id (first @ids)]
|
||||
(swap! ids rest)
|
||||
id)))
|
||||
batch (#'sut/build-page-and-blocks-batch 10 2 3 next-id)]
|
||||
(is (= 2 (count batch)))
|
||||
(is (= ["Page-10" "Page-11"]
|
||||
(map (comp :block/title :page) batch)))
|
||||
(is (= ["id-0" "id-4"]
|
||||
(map (comp :block/uuid :page) batch)))
|
||||
(is (= [[(#'sut/build-block-title 10 0) (#'sut/build-block-title 10 1) (#'sut/build-block-title 10 2)]
|
||||
[(#'sut/build-block-title 11 0) (#'sut/build-block-title 11 1) (#'sut/build-block-title 11 2)]]
|
||||
(map (fn [{:keys [blocks]}]
|
||||
(mapv :block/title blocks))
|
||||
batch)))
|
||||
(is (= [["id-1" "id-2" "id-3"]
|
||||
["id-5" "id-6" "id-7"]]
|
||||
(map (fn [{:keys [blocks]}]
|
||||
(mapv :block/uuid blocks))
|
||||
batch)))))
|
||||
|
||||
(deftest page-and-block-batches-only-realize-requested-batches
|
||||
(let [calls (atom 0)
|
||||
next-id (fn []
|
||||
(swap! calls inc)
|
||||
(str "id-" @calls))
|
||||
batches (#'sut/page-and-block-batches {:pages 50000
|
||||
:blocks 50
|
||||
:batch-pages 100}
|
||||
next-id)
|
||||
first-batch (first batches)]
|
||||
(is (= 100 (count first-batch)))
|
||||
(is (= (* 100 51) @calls)
|
||||
"Only the first batch should be realized")
|
||||
(is (= "Page-0" (get-in first-batch [0 :page :block/title])))
|
||||
(is (= "Page-99" (get-in first-batch [99 :page :block/title])))))
|
||||
|
||||
(deftest default-batching-keeps-large-graphs-bounded
|
||||
(testing "50k pages with 50 blocks are split into many batches instead of one giant tx"
|
||||
(let [batch-pages (#'sut/default-batch-pages 50)]
|
||||
(is (< batch-pages 50000))
|
||||
(is (pos? batch-pages))
|
||||
(is (= batch-pages
|
||||
(count (first (#'sut/page-and-block-batches {:pages 50000
|
||||
:blocks 50}
|
||||
(constantly "id")))))))))
|
||||
|
||||
(deftest page-and-block-batches-handle-empty-input
|
||||
(is (= []
|
||||
(into [] (#'sut/page-and-block-batches {:pages 0
|
||||
:blocks 50}
|
||||
(constantly "id"))))))
|
||||
|
||||
(deftest parse-args-keeps-the-graph-name-separate-from-cli-options
|
||||
(let [{:keys [graph-dir options]} (sut/parse-args ["large-graph"
|
||||
"-p" "3"
|
||||
"-b" "2"
|
||||
"-t" "1"])]
|
||||
(is (= "large-graph" graph-dir))
|
||||
(is (= 3 (:pages options)))
|
||||
(is (= 2 (:blocks options)))
|
||||
(is (= 1 (:batch-pages options)))))
|
||||
12
scripts/test/logseq/tasks/dev_mobile_test.clj
Normal file
12
scripts/test/logseq/tasks/dev_mobile_test.clj
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns logseq.tasks.dev-mobile-test
|
||||
(:require [clojure.test :refer [deftest is run-tests]]
|
||||
[logseq.tasks.dev.mobile :as mobile]))
|
||||
|
||||
(deftest dev-server-url-keeps-mobile-assets-relative-to-mobile-path
|
||||
(is (= "https://192.0.2.1:3002/mobile/"
|
||||
(mobile/dev-server-url "192.0.2.1"))))
|
||||
|
||||
(defn -main
|
||||
[& _]
|
||||
(let [{:keys [fail error]} (run-tests 'logseq.tasks.dev-mobile-test)]
|
||||
(System/exit (+ fail error))))
|
||||
197
scripts/test/logseq/tasks/dev_test.clj
Normal file
197
scripts/test/logseq/tasks/dev_test.clj
Normal file
@@ -0,0 +1,197 @@
|
||||
(ns logseq.tasks.dev-test
|
||||
(:require [clojure.string :as string]
|
||||
[clojure.test :refer [deftest is run-tests]]
|
||||
[logseq.tasks.dev :as dev]
|
||||
[logseq.tasks.dev.lint :as dev-lint]))
|
||||
|
||||
(deftest lint-and-test-starts-tests-while-lint-is-running
|
||||
(let [lint-started (promise)
|
||||
test-started (promise)
|
||||
release-tasks (promise)
|
||||
parallel-test-args (atom nil)]
|
||||
(with-redefs [dev-lint/dev (fn []
|
||||
(deliver lint-started true)
|
||||
@test-started
|
||||
@release-tasks)
|
||||
dev/parallel-test (fn [& args]
|
||||
(reset! parallel-test-args args)
|
||||
(deliver test-started true)
|
||||
@lint-started
|
||||
@release-tasks)]
|
||||
(let [runner (future (dev/lint-and-test))]
|
||||
(is (deref lint-started 1000 false))
|
||||
(is (deref test-started 100 false))
|
||||
(deliver test-started true)
|
||||
(deliver release-tasks true)
|
||||
@runner
|
||||
(is (= ["-e" "long" "-e" "fix-me"] @parallel-test-args))))))
|
||||
|
||||
(def test-namespaces
|
||||
["frontend.alpha-test"
|
||||
"frontend.beta-test"
|
||||
"frontend.gamma-test"
|
||||
"frontend.delta-test"
|
||||
"frontend.epsilon-test"])
|
||||
|
||||
(deftest run-test-namespaces-batches-namespaces-across-workers
|
||||
(let [started-node-runs (atom [])]
|
||||
(with-redefs [dev/test-jobs 2
|
||||
dev/test-batches-per-job 1
|
||||
dev/run-shell (fn [& args]
|
||||
(let [cmd-args (mapv str args)]
|
||||
(if (some #{"--list-namespaces"} cmd-args)
|
||||
{:out (string/join "\n" test-namespaces)}
|
||||
(swap! started-node-runs conj cmd-args))))]
|
||||
(dev/run-test-namespaces)
|
||||
(is (= 2 (count @started-node-runs)))
|
||||
(is (= (set test-namespaces)
|
||||
(->> @started-node-runs
|
||||
(mapcat identity)
|
||||
(filter (set test-namespaces))
|
||||
set))))))
|
||||
|
||||
(deftest run-test-namespaces-isolates-stateful-namespaces
|
||||
(let [namespaces ["frontend.alpha-test"
|
||||
"frontend.components.block.drop-boundary-test"
|
||||
"frontend.handler.editor-test"
|
||||
"frontend.worker.db-core-test"]
|
||||
started-node-runs (atom [])]
|
||||
(with-redefs [dev/test-jobs 2
|
||||
dev/test-batches-per-job 1
|
||||
dev/run-shell (fn [& args]
|
||||
(let [cmd-args (mapv str args)]
|
||||
(if (some #{"--list-namespaces"} cmd-args)
|
||||
{:out (string/join "\n" namespaces)}
|
||||
(swap! started-node-runs conj cmd-args))))]
|
||||
(dev/run-test-namespaces)
|
||||
(is (= 4 (count @started-node-runs)))
|
||||
(doseq [isolated-ns ["frontend.components.block.drop-boundary-test"
|
||||
"frontend.handler.editor-test"
|
||||
"frontend.worker.db-core-test"]]
|
||||
(is (= 1 (count (filter #(some #{isolated-ns} %) @started-node-runs))))
|
||||
(is (= 1 (->> @started-node-runs
|
||||
(some #(when (some #{isolated-ns} %) %))
|
||||
(filter (set namespaces))
|
||||
count)))))))
|
||||
|
||||
(deftest run-test-namespaces-runs-performance-tests-after-parallel-tests
|
||||
(let [namespaces ["frontend.alpha-test"
|
||||
"frontend.db.query-dsl-test"
|
||||
"frontend.worker.search-test"]
|
||||
completed-runs (atom [])
|
||||
parallel-finished? (atom false)]
|
||||
(with-redefs [dev/test-jobs 2
|
||||
dev/test-batches-per-job 1
|
||||
dev/run-shell (fn [& args]
|
||||
(let [cmd-args (mapv str args)]
|
||||
(if (some #{"--list-namespaces"} cmd-args)
|
||||
{:out (string/join "\n" namespaces)}
|
||||
(let [selected (filter (set namespaces) cmd-args)]
|
||||
(when (some #{"frontend.alpha-test"} selected)
|
||||
(reset! parallel-finished? true))
|
||||
(when (some dev/serial-test-namespaces selected)
|
||||
(is @parallel-finished?))
|
||||
(swap! completed-runs into selected)))))]
|
||||
(dev/run-test-namespaces)
|
||||
(is (= namespaces @completed-runs)))))
|
||||
|
||||
(deftest run-test-namespaces-handles-an-empty-selection
|
||||
(let [node-runs (atom 0)]
|
||||
(with-redefs [dev/test-jobs 2
|
||||
dev/test-batches-per-job 1
|
||||
dev/run-shell (fn [& args]
|
||||
(if (some #{"--list-namespaces"} (map str args))
|
||||
{:out ""}
|
||||
(swap! node-runs inc)))]
|
||||
(dev/run-test-namespaces)
|
||||
(is (zero? @node-runs)))))
|
||||
|
||||
(deftest parallel-test-runs-test-namespaces-concurrently
|
||||
(let [compile-ran? (atom false)
|
||||
listed-namespaces? (atom false)
|
||||
started-node-runs (atom [])
|
||||
release-node-runs (promise)
|
||||
wait-for-started-runs (fn [n]
|
||||
(loop [remaining-attempts 20]
|
||||
(when (and (< (count @started-node-runs) n)
|
||||
(pos? remaining-attempts))
|
||||
(Thread/sleep 50)
|
||||
(recur (dec remaining-attempts)))))]
|
||||
(with-redefs [dev/test-jobs 2
|
||||
dev/test-batches-per-job 1
|
||||
dev/run-shell (fn [& args]
|
||||
(let [cmd-text (string/join " " (map str args))]
|
||||
(cond
|
||||
(string/includes? cmd-text "pnpm cljs:test")
|
||||
(reset! compile-ran? true)
|
||||
|
||||
(string/includes? cmd-text "--list-namespaces")
|
||||
(do
|
||||
(is (string/includes? cmd-text "^(?!logseq.db-sync.).*"))
|
||||
(reset! listed-namespaces? true)
|
||||
{:out (string/join "\n" test-namespaces)})
|
||||
|
||||
(string/includes? cmd-text "static/tests.js")
|
||||
(do
|
||||
(swap! started-node-runs conj args)
|
||||
@release-node-runs))))]
|
||||
(let [runner (future (dev/parallel-test "-e" "long" "-e" "fix-me"))]
|
||||
(wait-for-started-runs dev/test-jobs)
|
||||
(is @compile-ran?)
|
||||
(is @listed-namespaces?)
|
||||
(is (= dev/test-jobs (count @started-node-runs)))
|
||||
(is (every? #(some #{"-n"} (map str %)) @started-node-runs))
|
||||
(is (every? #(= ["-e" "long" "-e" "fix-me"]
|
||||
(take-last 4 (mapv str %)))
|
||||
@started-node-runs))
|
||||
(deliver release-node-runs true)
|
||||
@runner
|
||||
(is (= dev/test-jobs (count @started-node-runs)))
|
||||
(is (= (set test-namespaces)
|
||||
(->> @started-node-runs
|
||||
(mapcat #(map str %))
|
||||
(filter (set test-namespaces))
|
||||
set)))))))
|
||||
|
||||
(deftest parallel-test-reraises-namespace-failure-after-running-namespaces
|
||||
(let [started-node-runs (atom [])
|
||||
release-node-runs (promise)
|
||||
wait-for-started-runs (fn [n]
|
||||
(loop [remaining-attempts 20]
|
||||
(when (and (< (count @started-node-runs) n)
|
||||
(pos? remaining-attempts))
|
||||
(Thread/sleep 50)
|
||||
(recur (dec remaining-attempts)))))]
|
||||
(with-redefs [dev/test-jobs 2
|
||||
dev/test-batches-per-job 1
|
||||
dev/run-shell (fn [& args]
|
||||
(let [cmd-text (string/join " " (map str args))]
|
||||
(cond
|
||||
(string/includes? cmd-text "--list-namespaces")
|
||||
(do
|
||||
(is (string/includes? cmd-text "^(?!logseq.db-sync.).*"))
|
||||
{:out (string/join "\n" test-namespaces)})
|
||||
|
||||
(string/includes? cmd-text "static/tests.js")
|
||||
(let [run-number (count (swap! started-node-runs conj args))]
|
||||
@release-node-runs
|
||||
(when (= 2 run-number)
|
||||
(throw (ex-info "namespace failed" {})))))))]
|
||||
(let [runner (future
|
||||
(try
|
||||
(dev/parallel-test "-e" "long" "-e" "fix-me")
|
||||
:passed
|
||||
(catch Throwable e
|
||||
e)))]
|
||||
(wait-for-started-runs dev/test-jobs)
|
||||
(is (= dev/test-jobs (count @started-node-runs)))
|
||||
(deliver release-node-runs true)
|
||||
(let [result @runner]
|
||||
(is (= dev/test-jobs (count @started-node-runs)))
|
||||
(is (instance? clojure.lang.ExceptionInfo result))
|
||||
(is (= "namespace failed" (ex-message result))))))))
|
||||
|
||||
(defn -main
|
||||
[& _]
|
||||
(let [{:keys [fail error]} (run-tests 'logseq.tasks.dev-test)]
|
||||
(System/exit (+ fail error))))
|
||||
127
scripts/test/logseq/tasks/lang_test.cljs
Normal file
127
scripts/test/logseq/tasks/lang_test.cljs
Normal file
@@ -0,0 +1,127 @@
|
||||
(ns logseq.tasks.lang-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[logseq.tasks.lang-lint :as lang-lint]))
|
||||
|
||||
(deftest translation-placeholders-detect-placeholder-sets
|
||||
(is (= #{"1" "2"}
|
||||
(lang-lint/translation-placeholders "Open {1} from {2}")))
|
||||
(is (= #{}
|
||||
(lang-lint/translation-placeholders "Search with Google"))))
|
||||
|
||||
(deftest placeholder-mismatch-findings-detect-non-default-locale-errors
|
||||
(testing "a localized value must match English placeholders exactly once it is defined"
|
||||
(let [findings (lang-lint/placeholder-mismatch-findings
|
||||
{:en {:electron/link-open-confirm "Are you sure?\n\n{1}"
|
||||
:electron/write-file-failed-with-backup "Write failed {1} {2} {3}."}
|
||||
:zh-Hant {:electron/link-open-confirm "確定要開啟此外部連結嗎?"
|
||||
:electron/write-file-failed-with-backup "寫入失敗。備份檔案:{1}"}
|
||||
:zh-CN {:electron/link-open-confirm "确定要打开此链接吗?\n\n{1}"
|
||||
:electron/write-file-failed-with-backup "写入文件 {1} 失败,{2}。备份文件已保存到 {3}。"}})]
|
||||
(is (= [{:lang :zh-Hant
|
||||
:translation-key :electron/link-open-confirm
|
||||
:expected-placeholders ["1"]
|
||||
:actual-placeholders []
|
||||
:default-value "Are you sure?\n\n{1}"
|
||||
:localized-value "確定要開啟此外部連結嗎?"}
|
||||
{:lang :zh-Hant
|
||||
:translation-key :electron/write-file-failed-with-backup
|
||||
:expected-placeholders ["1" "2" "3"]
|
||||
:actual-placeholders ["1"]
|
||||
:default-value "Write failed {1} {2} {3}."
|
||||
:localized-value "寫入失敗。備份檔案:{1}"}]
|
||||
findings)))))
|
||||
|
||||
(deftest translation-rich-validation-findings-report-rich-contract-mismatches
|
||||
(testing "a localized rich translation must remain a zero-arg function once defined"
|
||||
(is (= [{:lang :zh-Hant
|
||||
:translation-key :e2ee/cloud-password-rich
|
||||
:expected-value-kind :fn
|
||||
:actual-value-kind :string}
|
||||
{:lang :zh-Hant
|
||||
:translation-key :on-boarding/main-title
|
||||
:expected-value-kind :fn
|
||||
:actual-value-kind :string}]
|
||||
(lang-lint/rich-translation-mismatch-findings
|
||||
{:en {:on-boarding/main-title (fn [] ["Welcome to " [:strong "Logseq!"]])
|
||||
:e2ee/cloud-password-rich (fn [] ["Cloud sentence " [:span "Local sentence"]])
|
||||
:e2ee/remember-password-rich (fn [] [[:span "Remember "] "your password."])}
|
||||
:zh-Hant {:on-boarding/main-title "歡迎使用 Logseq"
|
||||
:e2ee/cloud-password-rich "雲端密碼"
|
||||
:e2ee/remember-password-rich (fn [] [[:span "請記住"] "你的密碼。"])}})))))
|
||||
|
||||
(deftest identical-translation-findings-report-defined-values-equal-to-english
|
||||
(is (= [{:lang :ko
|
||||
:translation-key :ui/cancel
|
||||
:default-value "Cancel"}
|
||||
{:lang :ko
|
||||
:translation-key :ui/save
|
||||
:default-value "Save"}]
|
||||
(lang-lint/identical-translation-findings
|
||||
{:en {:ui/cancel "Cancel"
|
||||
:ui/save "Save"
|
||||
:ui/close "Close"}
|
||||
:ko {:ui/cancel "Cancel"
|
||||
:ui/save "Save"
|
||||
:ui/close "닫기"}
|
||||
:fr {:ui/cancel "Annuler"}}
|
||||
:ko))))
|
||||
|
||||
(deftest identical-translation-stats-count-defined-values-equal-to-english
|
||||
(is (= [{:lang :en
|
||||
:translation-count 2
|
||||
:same-as-en-count 2}
|
||||
{:lang :ko
|
||||
:translation-count 2
|
||||
:same-as-en-count 1}
|
||||
{:lang :fr
|
||||
:translation-count 1
|
||||
:same-as-en-count 0}]
|
||||
(lang-lint/identical-translation-stats
|
||||
{:en {:ui/cancel "Cancel"
|
||||
:ui/save "Save"}
|
||||
:ko {:ui/cancel "Cancel"
|
||||
:ui/save "저장"}
|
||||
:fr {:ui/cancel "Annuler"}}))))
|
||||
|
||||
(deftest translation-summary-stats-report-untranslated-and-same-as-en-count
|
||||
(is (= [{:lang :en
|
||||
:translation-count 4
|
||||
:untranslated-count nil
|
||||
:same-as-en-count nil}
|
||||
{:lang :fr
|
||||
:translation-count 2
|
||||
:untranslated-count 2
|
||||
:same-as-en-count 1}
|
||||
{:lang :ko
|
||||
:translation-count 3
|
||||
:untranslated-count 1
|
||||
:same-as-en-count 2}]
|
||||
(->> (lang-lint/translation-summary-stats
|
||||
{:en {:ui/cancel "Cancel"
|
||||
:ui/save "Save"
|
||||
:ui/close "Close"
|
||||
:ui/delete "Delete"}
|
||||
:ko {:ui/cancel "Cancel"
|
||||
:ui/save "저장"
|
||||
:ui/close "Close"}
|
||||
:fr {:ui/cancel "Annuler"
|
||||
:ui/save "Save"}})
|
||||
(sort-by :lang)
|
||||
vec))))
|
||||
|
||||
(deftest sort-translation-summary-stats-keeps-en-first-then-sorts-by-untranslated-and-same-as-en-count
|
||||
(is (= [:en :ko :fr :zh-Hant]
|
||||
(->> [{:lang :fr
|
||||
:untranslated-count 3
|
||||
:same-as-en-count 1}
|
||||
{:lang :en
|
||||
:untranslated-count nil
|
||||
:same-as-en-count nil}
|
||||
{:lang :zh-Hant
|
||||
:untranslated-count 1
|
||||
:same-as-en-count 3}
|
||||
{:lang :ko
|
||||
:untranslated-count 3
|
||||
:same-as-en-count 2}]
|
||||
lang-lint/sort-translation-summary-stats
|
||||
(mapv :lang)))))
|
||||
15
scripts/test/logseq/tasks/test_runner.cljs
Normal file
15
scripts/test/logseq/tasks/test_runner.cljs
Normal file
@@ -0,0 +1,15 @@
|
||||
(ns logseq.tasks.test-runner
|
||||
(:require [cljs.test :as test]
|
||||
[logseq.tasks.db-graph.create-graph-with-clojure-irc-history-test]
|
||||
[logseq.tasks.db-graph.create-graph-with-large-sizes-test]
|
||||
[logseq.tasks.lang-test]
|
||||
[logseq.tasks.util-test]))
|
||||
|
||||
(defn -main [& _]
|
||||
(let [{:keys [fail error]}
|
||||
(test/run-tests 'logseq.tasks.db-graph.create-graph-with-large-sizes-test
|
||||
'logseq.tasks.db-graph.create-graph-with-clojure-irc-history-test
|
||||
'logseq.tasks.lang-test
|
||||
'logseq.tasks.util-test)]
|
||||
(when (pos? (+ fail error))
|
||||
(js/process.exit 1))))
|
||||
19
scripts/test/logseq/tasks/util_test.cljs
Normal file
19
scripts/test/logseq/tasks/util_test.cljs
Normal file
@@ -0,0 +1,19 @@
|
||||
(ns logseq.tasks.util-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[logseq.tasks.util :as util]))
|
||||
|
||||
(deftest display-width-handles-wide-and-combining-characters
|
||||
(is (= 5 (util/display-width "hello")))
|
||||
(is (= 4 (util/display-width "中文")))
|
||||
(is (= 4 (util/display-width "한국")))
|
||||
(is (= 1 (util/display-width "e\u0301"))))
|
||||
|
||||
(deftest render-table-right-aligns-columns-using-display-width
|
||||
(is (= (str "| :locale | :language |\n"
|
||||
"|---------+-----------|\n"
|
||||
"| :en | English |\n"
|
||||
"| :ja | 日本語 |\n"
|
||||
"| :ko | 한국 |\n")
|
||||
(util/render-table [{:locale :en :language "English"}
|
||||
{:locale :ja :language "日本語"}
|
||||
{:locale :ko :language "한국"}]))))
|
||||
Reference in New Issue
Block a user