Skip to content

Commit 099215a

Browse files
committed
Guard large cartesian generation and refine test env badge
1 parent 65f4665 commit 099215a

13 files changed

Lines changed: 258 additions & 43 deletions

File tree

apps/web/src/combinatorial-entry.mjs

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
CombinationAlgorithm,
1111
calculateCartesianProductRows,
1212
COMBINATION_STRATEGIES,
13+
confirmCartesianProductSelection,
1314
getAvailableStrengths,
1415
getStrategiesForStrength,
1516
} from '../../../packages/core-ui/js/gui_components/generator/generation/n-wise-generation-options.js';
@@ -32,8 +33,6 @@ const DEFAULT_SELECTED_ALGORITHMS = new Set([
3233
CombinationAlgorithm.IPOG,
3334
]);
3435

35-
const CARTESIAN_CONFIRM_THRESHOLD = 10000;
36-
3736
function escapeHtml(value) {
3837
return String(value ?? '')
3938
.replace(/&/g, '&')
@@ -173,30 +172,17 @@ function sortResultsByRowCount(results) {
173172
});
174173
}
175174

176-
async function filterAlgorithmsForCartesianConfirmation({
177-
algorithms,
178-
parameters,
179-
requestConfirm,
180-
threshold = CARTESIAN_CONFIRM_THRESHOLD,
181-
}) {
175+
async function filterAlgorithmsForCartesianConfirmation({ algorithms, parameters, requestConfirm, threshold }) {
182176
const selectedAlgorithms = Array.isArray(algorithms) ? [...algorithms] : [];
183177
if (!selectedAlgorithms.includes(CombinationAlgorithm.CARTESIAN_PRODUCT)) {
184178
return selectedAlgorithms;
185179
}
186180

187-
const cartesianRowCount = calculateCartesianProductRows(getValueCounts(parameters));
188-
if (typeof requestConfirm !== 'function') {
189-
return selectedAlgorithms;
190-
}
191-
if (Number.isFinite(cartesianRowCount) && cartesianRowCount <= threshold) {
192-
return selectedAlgorithms;
193-
}
194-
195-
const confirmed = await requestConfirm({
196-
title: 'Cartesian product generation',
197-
message: `You included cartesian product generation. Are you sure? this will generate ${formatNumber(cartesianRowCount)} data rows.`,
198-
okLabel: 'Run cartesian product',
199-
cancelLabel: 'Skip cartesian product',
181+
const confirmed = await confirmCartesianProductSelection({
182+
algorithm: CombinationAlgorithm.CARTESIAN_PRODUCT,
183+
valueCounts: getValueCounts(parameters),
184+
requestConfirm,
185+
threshold,
200186
});
201187

202188
return confirmed

apps/web/src/tests/browser/app/functional/test-data/pairwise-generation.spec.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,34 @@ test.describe('7. Test Data Generation', () => {
119119
expect(actualTriples).toEqual(expectedTriples);
120120
expectNoPageErrors(pageErrors);
121121
});
122+
123+
test('large cartesian combination runs require confirmation before generating', async ({ page }) => {
124+
const { appPage, pageErrors } = await openApp(page);
125+
126+
await appPage.testDataPanel.expand();
127+
await appPage.testDataPanel.expectExpanded();
128+
129+
await appPage.testDataPanel.setSchemaText(
130+
'A\nenum(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)\n\nB\nenum(b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11)\n\nC\nenum(c1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11)\n\nD\nenum(d1,d2,d3,d4,d5,d6,d7,d8,d9,d10,d11)'
131+
);
132+
133+
const initialRowCount = await appPage.gridEditor.renderer.countRows();
134+
135+
await appPage.testDataPanel.openGenerateCombinationsDialog();
136+
await appPage.testDataPanel.setCombinationStrength(4);
137+
await appPage.testDataPanel.chooseCombinationStrategy('Cartesian Product');
138+
await appPage.testDataPanel.submitGenerateCombinationsDialog();
139+
140+
const confirmDialog = page.getByRole('dialog', { name: 'Cartesian product generation' });
141+
await expect(confirmDialog).toBeVisible();
142+
await expect(confirmDialog.getByText(/14,641 data rows/i)).toBeVisible();
143+
await confirmDialog.getByRole('button', { name: 'Skip cartesian product' }).click();
144+
145+
await expect(confirmDialog).toBeHidden();
146+
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(initialRowCount);
147+
await expect
148+
.poll(async () => appPage.testDataPanel.getStatusText())
149+
.toContain('Cartesian product generation skipped.');
150+
expectNoPageErrors(pageErrors);
151+
});
122152
});

docs-src/test-build-seo.cjs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ const TEST_BUILD_INDICATOR_STYLE = `<style data-testenv-indicator>
77
body::before {
88
content: "Test Environment";
99
position: fixed;
10-
top: 12px;
11-
left: 12px;
10+
top: 0;
11+
left: 0;
1212
z-index: 2147483647;
13-
padding: 0.45rem 0.75rem;
14-
border: 1px solid rgba(110, 35, 12, 0.35);
15-
border-radius: 999px;
13+
margin: 0;
14+
padding: 0.225rem 0.375rem;
15+
border: 0;
16+
border-radius: 0;
1617
background: rgba(255, 244, 214, 0.96);
1718
color: #6e230c;
18-
box-shadow: 0 10px 24px rgba(110, 35, 12, 0.18);
19-
font: 700 12px/1.2 Arial, Helvetica, sans-serif;
19+
box-shadow: none;
20+
font: 700 6px/1.2 Arial, Helvetica, sans-serif;
2021
letter-spacing: 0.08em;
2122
text-transform: uppercase;
2223
pointer-events: none;

packages/core-ui/js/gui_components/app/test-data-grid/controller/test-data-grid-controller.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { createTestDataGridActionAdapter } from '../controller/test-data-grid-ac
4747
import { createDataPopulationPanelComponent } from '../../data-population-panel/index.js';
4848
import { resolveDocumentObj } from '../../../shared/dom/default-objects.js';
4949
import { createCombinationsDialogComponent } from '../../../shared/combinations-dialog/index.js';
50+
import { createConfirmDialogService } from '../../../shared/dialog-services/confirm-dialog-service.js';
5051

5152
import { faker } from 'https://cdn.skypack.dev/@faker-js/faker@v9.7.0';
5253

@@ -148,6 +149,7 @@ function createTestDataGenerationPanelManager({
148149
function createGenerationService() {
149150
const resolvedRandExpClass = getRandExpClass();
150151
const statusServiceApi = getStatusServiceApi();
152+
const confirmDialogService = createConfirmDialogService({ documentObj: getResolvedDocument() });
151153
return createTestDataGenerationServiceFn({
152154
schemaTextToDataRules: schemaTextToDataRulesFn,
153155
schemaRowsToSpec: (schemaRows) =>
@@ -182,6 +184,7 @@ function createTestDataGenerationPanelManager({
182184
setGenerateBusy: (isBusy) => state.dataPopulationPanel?.setGenerateBusy?.(isBusy),
183185
setGeneratePairwiseBusy: (isBusy) => state.dataPopulationPanel?.setGeneratePairwiseBusy?.(isBusy),
184186
setPairwiseVisible: (isVisible) => state.dataPopulationPanel?.setPairwiseVisible?.(isVisible),
187+
requestConfirm: confirmDialogService.requestConfirm,
185188
});
186189
}
187190

packages/core-ui/js/gui_components/app/test-data-grid/generation/test-data-generation-service.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
CombinationAlgorithm,
1818
DEFAULT_AETG_RUNS,
1919
} from '@anywaydata/core/data_generation/n-wise/combinationsTestDataGenerator.js';
20+
import { confirmCartesianProductSelection } from '../../../generator/generation/n-wise-generation-options.js';
2021
import {
2122
SOURCE_TYPE_FAKER,
2223
SOURCE_TYPE_DOMAIN,
@@ -55,6 +56,7 @@ function createTestDataGenerationService({
5556
setGenerateBusy = () => {},
5657
setGeneratePairwiseBusy = () => {},
5758
setPairwiseVisible = () => {},
59+
requestConfirm,
5860
}) {
5961
function getCurrentSchemaRowValidation(options) {
6062
return validateCurrentSchemaRows?.(options) || { errors: [], rows: [] };
@@ -297,6 +299,16 @@ function createTestDataGenerationService({
297299
return;
298300
}
299301

302+
const confirmed = await confirmCartesianProductSelection({
303+
algorithm,
304+
valueCounts: getEnumValueCounts(),
305+
requestConfirm,
306+
});
307+
if (!confirmed) {
308+
setTestDataStatus('Cartesian product generation skipped.', { severity: 'warning', dismissable: true });
309+
return;
310+
}
311+
300312
await yieldToUi();
301313

302314
const dataTable = createCombinationsDataTable({

packages/core-ui/js/gui_components/generator/generation/data-generator-generation-actions.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
extractLiteralValueFromRuleSpec,
3232
extractRegexValueFromRuleSpec,
3333
} from '../../shared/schema-row-rule-mapper.js';
34+
import { confirmCartesianProductSelection } from './n-wise-generation-options.js';
3435

3536
function createConfiguredGeneratorForPage({
3637
syncSchemaRowsFromTextMode,
@@ -355,6 +356,7 @@ async function generateGeneratorAllPairsDataFile({
355356
async function generateGeneratorCombinationsDataFile({
356357
createConfiguredGenerator,
357358
countEnumColumns,
359+
getEnumValueCounts,
358360
getSelectedOutputType,
359361
exporter,
360362
clearGenerationStatus,
@@ -367,6 +369,7 @@ async function generateGeneratorCombinationsDataFile({
367369
clearPageError,
368370
scheduleClearGenerationStatus,
369371
selection,
372+
requestConfirm,
370373
}) {
371374
const strength = Number.parseInt(selection?.strength, 10);
372375
const algorithm = selection?.algorithm;
@@ -382,6 +385,16 @@ async function generateGeneratorCombinationsDataFile({
382385
return;
383386
}
384387

388+
const confirmed = await confirmCartesianProductSelection({
389+
algorithm,
390+
valueCounts: getEnumValueCounts?.() || [],
391+
requestConfirm,
392+
});
393+
if (!confirmed) {
394+
setGenerationStatus('Cartesian product generation skipped.', { severity: 'warning', dismissable: true });
395+
return;
396+
}
397+
385398
const type = getSelectedOutputType();
386399
if (!exporter.canExport(type)) {
387400
surfacePageError(`Output format ${type} is not supported.`);

packages/core-ui/js/gui_components/generator/generation/n-wise-generation-options.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from '@anywaydata/core/data_generation/n-wise/combinationsTestDataGenerator.js';
55

66
const N_WISE_DOCS_URL = '/docs/test-data/n-wise-testing';
7+
const CARTESIAN_CONFIRM_THRESHOLD = 10000;
78

89
const COMBINATION_STRATEGY_ROW_COUNT_ORDER = Object.freeze({
910
2: [
@@ -203,11 +204,50 @@ function getStrategyById(strategyId) {
203204
return COMBINATION_STRATEGIES.find((strategy) => strategy.id === strategyId) || null;
204205
}
205206

207+
function formatCombinationCount(value) {
208+
if (value === Number.POSITIVE_INFINITY) {
209+
return 'Too large to estimate';
210+
}
211+
if (value === Number.NEGATIVE_INFINITY) {
212+
return 'Too small to estimate';
213+
}
214+
return Number.isFinite(value) ? Number(value).toLocaleString('en-US') : 'Unknown';
215+
}
216+
217+
async function confirmCartesianProductSelection({
218+
algorithm,
219+
valueCounts = [],
220+
requestConfirm,
221+
threshold = CARTESIAN_CONFIRM_THRESHOLD,
222+
} = {}) {
223+
if (algorithm !== CombinationAlgorithm.CARTESIAN_PRODUCT) {
224+
return true;
225+
}
226+
227+
const cartesianRowCount = calculateCartesianProductRows(valueCounts);
228+
if (typeof requestConfirm !== 'function') {
229+
return true;
230+
}
231+
if (Number.isFinite(cartesianRowCount) && cartesianRowCount <= threshold) {
232+
return true;
233+
}
234+
235+
return requestConfirm({
236+
title: 'Cartesian product generation',
237+
message: `You included cartesian product generation. Are you sure? this will generate ${formatCombinationCount(cartesianRowCount)} data rows.`,
238+
okLabel: 'Run cartesian product',
239+
cancelLabel: 'Skip cartesian product',
240+
});
241+
}
242+
206243
export {
244+
CARTESIAN_CONFIRM_THRESHOLD,
207245
CombinationAlgorithm,
208246
N_WISE_DOCS_URL,
209247
COMBINATION_STRATEGIES,
210248
calculateCartesianProductRows,
249+
confirmCartesianProductSelection,
250+
formatCombinationCount,
211251
getAvailableStrengths,
212252
getStrengthExplanation,
213253
getStrategiesForStrength,

packages/core-ui/js/gui_components/generator/runtime/generator-page-actions-service.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '../generation/data-generator-generation-actions.js';
1010
import { createCombinationsDialogComponent } from '../../shared/combinations-dialog/index.js';
1111
import { applyGeneratorFormatOptions } from '../options/apply-generator-format-options.js';
12+
import { createConfirmDialogService } from '../../shared/dialog-services/confirm-dialog-service.js';
1213

1314
function createGeneratorPageActionsService({
1415
runtime,
@@ -17,6 +18,7 @@ function createGeneratorPageActionsService({
1718
RandExp,
1819
createCombinationsDialog = createCombinationsDialogComponent,
1920
} = {}) {
21+
const confirmDialogService = createConfirmDialogService({ documentObj: runtime?.documentObj });
2022
const getResolvedViewState = () => runtime?.generatorViewState || null;
2123
const getResolvedSchemaRuntime = () => runtime?.generatorSchemaRuntime || null;
2224
const getResolvedSchemaGenerationService = () => runtime?.generatorSchemaGenerationService || null;
@@ -126,6 +128,7 @@ function createGeneratorPageActionsService({
126128
await generateGeneratorCombinationsDataFile({
127129
createConfiguredGenerator: () => getResolvedSchemaGenerationService()?.createConfiguredGenerator?.(),
128130
countEnumColumns: () => getResolvedSchemaGenerationService()?.countEnumColumns?.() || 0,
131+
getEnumValueCounts: () => getResolvedSchemaGenerationService()?.getEnumValueCounts?.() || [],
129132
getSelectedOutputType: () => getResolvedViewState()?.getSelectedOutputType?.(),
130133
exporter: runtime?.exporter,
131134
clearGenerationStatus: () => getResolvedViewState()?.clearGenerationStatus?.(),
@@ -144,6 +147,7 @@ function createGeneratorPageActionsService({
144147
clearPageError: () => getResolvedSchemaRuntime()?.clearSchemaErrorStatus?.(),
145148
scheduleClearGenerationStatus: (delay) => getResolvedViewState()?.scheduleClearGenerationStatus?.(delay),
146149
selection,
150+
requestConfirm: confirmDialogService.requestConfirm,
147151
});
148152
},
149153

packages/core-ui/src/tests/generator/data-generator-generation-actions.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { jest } from '@jest/globals';
22
import { JSDOM } from 'jsdom';
33
import { createGeneratorSchemaGenerationService } from '../../../js/gui_components/generator/generation/generator-schema-generation-service.js';
44
import {
5+
generateGeneratorCombinationsDataFile,
56
generateGeneratorDataFile,
67
renderGeneratorOutputPreview,
78
updateGeneratorPairwiseButtonVisibility,
@@ -124,4 +125,46 @@ describe('generator generation actions', () => {
124125
text: '\uFEFFName\r\nAda',
125126
});
126127
});
128+
129+
test('generateGeneratorCombinationsDataFile prompts before large cartesian runs and skips when cancelled', async () => {
130+
const requestConfirm = jest.fn(async () => false);
131+
const setGenerationStatus = jest.fn();
132+
const buildCombinationsDataTable = jest.fn();
133+
134+
await generateGeneratorCombinationsDataFile({
135+
createConfiguredGenerator: () => ({
136+
generator: { testDataRules: () => [{}, {}, {}, {}] },
137+
}),
138+
countEnumColumns: () => 4,
139+
getEnumValueCounts: () => [11, 11, 11, 11],
140+
getSelectedOutputType: () => 'csv',
141+
exporter: {
142+
canExport: () => true,
143+
getFileExtensionFor: () => '.csv',
144+
},
145+
clearGenerationStatus: jest.fn(),
146+
setGenerationButtonBusy: jest.fn(),
147+
setGenerationStatus,
148+
showGenerationLoadingStatus: jest.fn(),
149+
buildCombinationsDataTable,
150+
DownloadClass: class FakeDownload {},
151+
surfacePageError: jest.fn(),
152+
clearPageError: jest.fn(),
153+
scheduleClearGenerationStatus: jest.fn(),
154+
selection: { strength: 4, algorithm: 'cartesian-product' },
155+
requestConfirm,
156+
});
157+
158+
expect(requestConfirm).toHaveBeenCalledWith({
159+
title: 'Cartesian product generation',
160+
message: expect.stringContaining('14,641'),
161+
okLabel: 'Run cartesian product',
162+
cancelLabel: 'Skip cartesian product',
163+
});
164+
expect(buildCombinationsDataTable).not.toHaveBeenCalled();
165+
expect(setGenerationStatus).toHaveBeenCalledWith('Cartesian product generation skipped.', {
166+
severity: 'warning',
167+
dismissable: true,
168+
});
169+
});
127170
});

0 commit comments

Comments
 (0)