plum-e2e 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/config/scripts/run-tests.js +4 -2
- package/backend/routes/node.routes.js +15 -5
- package/backend/services/runnerService.js +16 -1
- package/bin/plum.js +3 -3
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +2 -2
- package/frontend/src/lib/constants.js +0 -2
- package/frontend/src/routes/scheduled-tests/+page.svelte +73 -14
- package/package.json +1 -1
|
@@ -27,7 +27,9 @@ const runners = parallel || process.env.REPORT_RUNNERS || '1';
|
|
|
27
27
|
const tag = process.env.TAG || process.argv.slice(2).find((a) => a.startsWith('@'));
|
|
28
28
|
const browser = process.env.BROWSER || 'chromium';
|
|
29
29
|
|
|
30
|
-
const reportFile =
|
|
30
|
+
const reportFile =
|
|
31
|
+
process.env.CUCUMBER_REPORT_FILE ||
|
|
32
|
+
path.resolve(process.cwd(), 'reports', 'cucumber_report.json');
|
|
31
33
|
|
|
32
34
|
// Wipe any previous report so a crashed/empty run can never return stale results.
|
|
33
35
|
try {
|
|
@@ -87,7 +89,7 @@ try {
|
|
|
87
89
|
);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
baseCommand.push('--format',
|
|
92
|
+
baseCommand.push('--format', `json:${reportFile.replace(/\\/g, '/')}`);
|
|
91
93
|
|
|
92
94
|
if (tag) {
|
|
93
95
|
baseCommand.push('--tags', `"${tag}"`);
|
|
@@ -50,13 +50,18 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Each job writes to its own temp file so concurrent jobs on the same node
|
|
54
|
+
// cannot clobber each other's reports (shared cucumber_report.json race condition).
|
|
55
|
+
const reportFile = path.join(os.tmpdir(), `plum-report-${jobId}.json`);
|
|
56
|
+
|
|
53
57
|
jobs[jobId] = {
|
|
54
58
|
status: 'running',
|
|
55
59
|
logs: '',
|
|
56
60
|
exitCode: null,
|
|
57
61
|
startedAt: Date.now(),
|
|
58
62
|
meta: { tags: tags || '', browser, workers },
|
|
59
|
-
tempTestsDir
|
|
63
|
+
tempTestsDir,
|
|
64
|
+
reportFile
|
|
60
65
|
};
|
|
61
66
|
|
|
62
67
|
const env = {
|
|
@@ -65,6 +70,7 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
65
70
|
TRIGGER: TRIGGER_REMOTE,
|
|
66
71
|
BROWSER: browser,
|
|
67
72
|
REPORT_RUNNERS: String(workers),
|
|
73
|
+
CUCUMBER_REPORT_FILE: reportFile,
|
|
68
74
|
...(tempTestsDir ? { TESTS_ROOT: tempTestsDir } : {})
|
|
69
75
|
};
|
|
70
76
|
if (workers > 1) env.PARALLEL = String(workers);
|
|
@@ -81,9 +87,8 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
81
87
|
jobs[jobId].exitCode = code;
|
|
82
88
|
|
|
83
89
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
jobs[jobId].reportContent = fs.readFileSync(reportPath, 'utf8');
|
|
90
|
+
if (fs.existsSync(reportFile)) {
|
|
91
|
+
jobs[jobId].reportContent = fs.readFileSync(reportFile, 'utf8');
|
|
87
92
|
}
|
|
88
93
|
} catch {}
|
|
89
94
|
|
|
@@ -91,7 +96,12 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
91
96
|
fs.rm(jobs[jobId].tempTestsDir, { recursive: true, force: true }, () => {});
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
setTimeout(() =>
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
try {
|
|
101
|
+
fs.unlinkSync(reportFile);
|
|
102
|
+
} catch {}
|
|
103
|
+
delete jobs[jobId];
|
|
104
|
+
}, 600_000);
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
res.json({ jobId, status: 'started' });
|
|
@@ -28,7 +28,22 @@ const getAll = () => prisma.runner.findMany({ orderBy: { createdAt: 'asc' } });
|
|
|
28
28
|
const create = ({ name, url, token, browser = 'chromium' }) =>
|
|
29
29
|
prisma.runner.create({ data: { name, url, token, browser } });
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
async function remove(id) {
|
|
32
|
+
// Scrub the deleted runner from any cron job's runnerIds string before
|
|
33
|
+
// deleting, since that field has no relational constraint.
|
|
34
|
+
const jobs = await prisma.cronJob.findMany({ select: { id: true, runnerIds: true } });
|
|
35
|
+
for (const job of jobs) {
|
|
36
|
+
const ids = job.runnerIds
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter((s) => s && s !== id);
|
|
40
|
+
await prisma.cronJob.update({
|
|
41
|
+
where: { id: job.id },
|
|
42
|
+
data: { runnerIds: ids.length > 0 ? ids.join(',') : 'built-in' }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return prisma.runner.delete({ where: { id } });
|
|
46
|
+
}
|
|
32
47
|
|
|
33
48
|
const update = (id, data) => prisma.runner.update({ where: { id }, data });
|
|
34
49
|
|
package/bin/plum.js
CHANGED
|
@@ -323,7 +323,7 @@ async function configureNode({ force }) {
|
|
|
323
323
|
|
|
324
324
|
if (interactive) {
|
|
325
325
|
const primaryVal = await clack.text({
|
|
326
|
-
message: '
|
|
326
|
+
message: 'Your Plum server backend URL',
|
|
327
327
|
placeholder: primary || 'http://localhost:3001',
|
|
328
328
|
defaultValue: primary
|
|
329
329
|
});
|
|
@@ -331,7 +331,7 @@ async function configureNode({ force }) {
|
|
|
331
331
|
primary = primaryVal || primary;
|
|
332
332
|
|
|
333
333
|
const portVal = await clack.text({
|
|
334
|
-
message: 'Local port this node listens on',
|
|
334
|
+
message: 'Local port this Plum node listens on',
|
|
335
335
|
placeholder: port,
|
|
336
336
|
defaultValue: port
|
|
337
337
|
});
|
|
@@ -340,7 +340,7 @@ async function configureNode({ force }) {
|
|
|
340
340
|
|
|
341
341
|
const defaultUrl = url || `http://${detectLanIp()}:${port}`;
|
|
342
342
|
const urlVal = await clack.text({
|
|
343
|
-
message: 'URL
|
|
343
|
+
message: 'The URL your Plum server calls to communicate with this node',
|
|
344
344
|
placeholder: defaultUrl,
|
|
345
345
|
defaultValue: defaultUrl
|
|
346
346
|
});
|
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
function adjustWorkers(delta) {
|
|
235
235
|
runnerConfig.update((c) => ({
|
|
236
236
|
...c,
|
|
237
|
-
workers: Math.max(1, Math.min(
|
|
237
|
+
workers: Math.max(1, Math.min(10, c.workers + delta))
|
|
238
238
|
}));
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -353,7 +353,7 @@
|
|
|
353
353
|
<button
|
|
354
354
|
class="step-btn"
|
|
355
355
|
on:click={() => adjustWorkers(1)}
|
|
356
|
-
disabled={cfg.workers >=
|
|
356
|
+
disabled={cfg.workers >= 10 || state.running}>+</button
|
|
357
357
|
>
|
|
358
358
|
</div>
|
|
359
359
|
</div>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
} from '$lib/api/schedules';
|
|
27
27
|
import { fetchRunners } from '$lib/api/runners';
|
|
28
28
|
import { activeCronJobs } from '$lib/stores/runner';
|
|
29
|
-
import { BROWSERS,
|
|
29
|
+
import { BROWSERS, TOAST_TIMEOUT_MS } from '$lib/constants';
|
|
30
30
|
import { stagger } from '$lib/utils/format';
|
|
31
31
|
import Button from '$lib/components/ui/Button.svelte';
|
|
32
32
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
@@ -124,6 +124,10 @@
|
|
|
124
124
|
form.runnerIds = current.includes(id) ? current.filter((r) => r !== id) : [...current, id];
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function adjustFormWorkers(delta) {
|
|
128
|
+
form.workers = Math.max(1, Math.min(10, (form.workers || 1) + delta));
|
|
129
|
+
}
|
|
130
|
+
|
|
127
131
|
function openAddModal() {
|
|
128
132
|
isEditing = false;
|
|
129
133
|
editTaskName = '';
|
|
@@ -144,14 +148,16 @@
|
|
|
144
148
|
function openEditModal(job) {
|
|
145
149
|
isEditing = true;
|
|
146
150
|
editTaskName = job.taskName;
|
|
151
|
+
const validIds = new Set(['built-in', ...availableRunners.map((r) => r.id)]);
|
|
147
152
|
const storedIds = job.runnerIds ? job.runnerIds.split(',').map((s) => s.trim()) : ['built-in'];
|
|
153
|
+
const prunedIds = storedIds.filter((id) => validIds.has(id));
|
|
148
154
|
form = {
|
|
149
155
|
taskName: job.taskName,
|
|
150
156
|
cronExpression: job.cronExpression,
|
|
151
157
|
tags: job.tags,
|
|
152
158
|
workers: job.workers ?? 1,
|
|
153
159
|
browser: job.browser ?? 'chromium',
|
|
154
|
-
runnerIds:
|
|
160
|
+
runnerIds: prunedIds.length > 0 ? prunedIds : ['built-in']
|
|
155
161
|
};
|
|
156
162
|
const isPreset = scheduleOptions.some((o) => o.value === job.cronExpression);
|
|
157
163
|
useCustomCron = !isPreset;
|
|
@@ -303,18 +309,20 @@
|
|
|
303
309
|
<span>Workers</span>
|
|
304
310
|
<span class="field-hint">Parallel workers for this job</span>
|
|
305
311
|
</div>
|
|
306
|
-
<div class="
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
312
|
+
<div class="stepper">
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
class="step-btn"
|
|
316
|
+
on:click={() => adjustFormWorkers(-1)}
|
|
317
|
+
disabled={form.workers <= 1}>−</button
|
|
318
|
+
>
|
|
319
|
+
<span class="step-val">{form.workers}</span>
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
class="step-btn"
|
|
323
|
+
on:click={() => adjustFormWorkers(1)}
|
|
324
|
+
disabled={form.workers >= 10}>+</button
|
|
325
|
+
>
|
|
318
326
|
</div>
|
|
319
327
|
</div>
|
|
320
328
|
|
|
@@ -663,6 +671,57 @@
|
|
|
663
671
|
color: var(--fail);
|
|
664
672
|
}
|
|
665
673
|
|
|
674
|
+
/* Workers stepper */
|
|
675
|
+
.stepper {
|
|
676
|
+
display: flex;
|
|
677
|
+
align-items: center;
|
|
678
|
+
background: var(--bg-subtle);
|
|
679
|
+
border: 1px solid var(--border);
|
|
680
|
+
border-radius: var(--radius-sm);
|
|
681
|
+
overflow: hidden;
|
|
682
|
+
width: fit-content;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.step-btn {
|
|
686
|
+
width: 28px;
|
|
687
|
+
height: 30px;
|
|
688
|
+
display: flex;
|
|
689
|
+
align-items: center;
|
|
690
|
+
justify-content: center;
|
|
691
|
+
border: none;
|
|
692
|
+
background: transparent;
|
|
693
|
+
color: var(--text-muted);
|
|
694
|
+
cursor: pointer;
|
|
695
|
+
font-size: 0.875rem;
|
|
696
|
+
font-weight: 400;
|
|
697
|
+
line-height: 1;
|
|
698
|
+
transition:
|
|
699
|
+
color var(--duration-fast),
|
|
700
|
+
background var(--duration-fast);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.step-btn:hover:not(:disabled) {
|
|
704
|
+
color: var(--text);
|
|
705
|
+
background: var(--bg-elevated);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.step-btn:disabled {
|
|
709
|
+
opacity: 0.3;
|
|
710
|
+
cursor: default;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.step-val {
|
|
714
|
+
min-width: 28px;
|
|
715
|
+
text-align: center;
|
|
716
|
+
font-size: 0.85rem;
|
|
717
|
+
font-weight: 500;
|
|
718
|
+
color: var(--text);
|
|
719
|
+
font-family: 'JetBrains Mono', monospace;
|
|
720
|
+
line-height: 30px;
|
|
721
|
+
border-left: 1px solid var(--border);
|
|
722
|
+
border-right: 1px solid var(--border);
|
|
723
|
+
}
|
|
724
|
+
|
|
666
725
|
/* Segmented control */
|
|
667
726
|
.seg-control {
|
|
668
727
|
display: flex;
|