Skip to content

Commit da1eb28

Browse files
authored
fix: implement backwards compatibility with old sqlite formats (#658)
* feat: implement database versioning and migration support * test: commit v0 report * fix: use debug logging for snapshot saving instead of console * test: implement sqlite formats backwards compatibility e2e test * test: add unit tests for database migrations * chore: fix review issues * chore: update package-lock * fix: adjust player dimensions and hide player when streaming is available, but snapshots found * test: fix e2e tests
1 parent 97ee231 commit da1eb28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2532
-2415
lines changed

lib/adapters/event-handling/testplane/snapshots.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'fs';
22
import path from 'path';
33

44
import type {eventWithTime as RrwebEvent} from '@rrweb/types';
5+
import makeDebug from 'debug';
56
import fsExtra from 'fs-extra';
67
import _ from 'lodash';
78
import type Testplane from 'testplane';
@@ -14,6 +15,8 @@ import {AttachmentType, SnapshotAttachment, TestStepKey, SnapshotsSaver} from '.
1415
import {EventSource} from '../../../gui/event-source';
1516
import {ClientEvents} from '../../../gui/constants';
1617

18+
const debug = makeDebug('html-reporter:event-handling:snapshots');
19+
1720
export interface TestContext {
1821
testPath: string[];
1922
browserId: string;
@@ -86,12 +89,10 @@ export const finalizeSnapshotsForTest = async ({testResult, attempt, reportPath,
8689

8790
// Here we only check LastFailedRun, because in case of Off, we wouldn't even be here. LastFailedRun is the only case when we may want to not save snapshots.
8891
const shouldSave = RecordMode && recordConfig && (recordConfig.mode !== RecordMode.LastFailedRun || (eventName === events.TEST_FAIL));
89-
if (!shouldSave) {
90-
return [];
91-
}
92-
93-
if (!snapshots || snapshots.length === 0) {
94-
console.warn(`No snapshots found for test hash: ${hash}`);
92+
if (!shouldSave || !snapshots || snapshots.length === 0) {
93+
debug('Not saving snapshots for test "%s"', hash);
94+
debug('shouldSave evaluated to: %s', shouldSave, ', recordConfig: ', recordConfig, ', eventName: ', eventName);
95+
debug('snapshots: ', snapshots);
9596
return [];
9697
}
9798

lib/constants/database.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ export const SUITES_TABLE_COLUMNS = [
3838
{name: DB_COLUMNS.ATTACHMENTS, type: DB_TYPES.text}
3939
] as const;
4040

41+
export const DB_VERSION_TABLE_NAME = 'version';
42+
export const VERSION_TABLE_COLUMNS = [
43+
{name: 'version_number', type: DB_TYPES.int}
44+
] as const;
45+
46+
export type LabeledVersionRow = {
47+
[K in (typeof VERSION_TABLE_COLUMNS)[number]['name']]: string;
48+
};
49+
50+
export const DB_CURRENT_VERSION = 1;
51+
4152
export const DB_MAX_AVAILABLE_PAGE_SIZE = 65536; // helps to speed up queries
4253
export const DB_SUITES_TABLE_NAME = 'suites';
4354
export const LOCAL_DATABASE_NAME = 'sqlite.db';

lib/db-utils/common.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import _ from 'lodash';
22
import {logger} from '../common-utils';
3-
import {DB_MAX_AVAILABLE_PAGE_SIZE, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, DB_COLUMN_INDEXES} from '../constants';
3+
import {DB_MAX_AVAILABLE_PAGE_SIZE, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, DB_COLUMN_INDEXES, DB_VERSION_TABLE_NAME, VERSION_TABLE_COLUMNS} from '../constants';
44
import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types';
55
import type {Database as BetterSqlite3Database, Statement} from 'better-sqlite3';
66
import {ReadonlyDeep} from 'type-fest';
@@ -9,7 +9,8 @@ export const selectAllQuery = (tableName: string): string => `SELECT * FROM ${ta
99
export const selectAllSuitesQuery = (): string => selectAllQuery(DB_SUITES_TABLE_NAME);
1010

1111
export const createTablesQuery = (): string[] => [
12-
createTableQuery(DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS)
12+
createTableQuery(DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS),
13+
createTableQuery(DB_VERSION_TABLE_NAME, VERSION_TABLE_COLUMNS)
1314
];
1415

1516
export const compareDatabaseRowsByTimestamp = (row1: RawSuitesRow, row2: RawSuitesRow): number => {
@@ -86,7 +87,7 @@ export const mergeTables = ({db, dbPaths, getExistingTables = (): string[] => []
8687
}
8788
};
8889

89-
function createTableQuery(tableName: string, columns: ReadonlyDeep<{name: string, type: string }[]>): string {
90+
export function createTableQuery(tableName: string, columns: ReadonlyDeep<{name: string, type: string }[]>): string {
9091
const formattedColumns = columns
9192
.map(({name, type}) => `${name} ${type}`)
9293
.join(', ');

lib/db-utils/migrations.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import fs from 'fs-extra';
2+
import makeDebug from 'debug';
3+
import Database from 'better-sqlite3';
4+
import {
5+
DB_VERSION_TABLE_NAME,
6+
DB_CURRENT_VERSION,
7+
DB_SUITES_TABLE_NAME,
8+
DB_COLUMNS,
9+
VERSION_TABLE_COLUMNS,
10+
LabeledVersionRow
11+
} from '../constants/database';
12+
import {isEqual} from 'lodash';
13+
import {createTableQuery} from './common';
14+
const debug = makeDebug('html-reporter:db-migrations');
15+
16+
export const getDatabaseVersion = (db: Database.Database): number | null => {
17+
debug('getDatabaseVersion');
18+
try {
19+
const versionTableExists = db.prepare(
20+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
21+
).get(DB_VERSION_TABLE_NAME);
22+
debug('versionTableExists:', versionTableExists);
23+
24+
if (versionTableExists) {
25+
const versionRow = db.prepare(`SELECT ${VERSION_TABLE_COLUMNS[0].name} FROM ${DB_VERSION_TABLE_NAME} LIMIT 1`).get() as LabeledVersionRow | null;
26+
debug('versionRow: ', versionRow);
27+
28+
const version = Number(versionRow?.[VERSION_TABLE_COLUMNS[0].name]);
29+
if (!isNaN(version) && version >= 0) {
30+
return version;
31+
}
32+
}
33+
34+
const tableInfo = db.prepare(`PRAGMA table_info(${DB_SUITES_TABLE_NAME})`).all() as {name: string}[];
35+
const columnNames = tableInfo.map(col => col.name);
36+
debug('column names in db:', columnNames);
37+
38+
const version0Columns = [
39+
DB_COLUMNS.SUITE_PATH,
40+
DB_COLUMNS.SUITE_NAME,
41+
DB_COLUMNS.NAME,
42+
DB_COLUMNS.SUITE_URL,
43+
DB_COLUMNS.META_INFO,
44+
DB_COLUMNS.HISTORY,
45+
DB_COLUMNS.DESCRIPTION,
46+
DB_COLUMNS.ERROR,
47+
DB_COLUMNS.SKIP_REASON,
48+
DB_COLUMNS.IMAGES_INFO,
49+
DB_COLUMNS.SCREENSHOT,
50+
DB_COLUMNS.MULTIPLE_TABS,
51+
DB_COLUMNS.STATUS,
52+
DB_COLUMNS.TIMESTAMP,
53+
DB_COLUMNS.DURATION
54+
];
55+
56+
if (isEqual(columnNames, version0Columns)) {
57+
return 0;
58+
}
59+
60+
if (isEqual(columnNames, [...version0Columns, DB_COLUMNS.ATTACHMENTS])) {
61+
return 1;
62+
}
63+
} catch (error) {
64+
debug(`Error getting database version: ${error}`);
65+
}
66+
return null;
67+
};
68+
69+
export const setDatabaseVersion = (db: Database.Database, version: number): void => {
70+
db.prepare(`DELETE FROM ${DB_VERSION_TABLE_NAME}`).run();
71+
db.prepare(`INSERT INTO ${DB_VERSION_TABLE_NAME} (${VERSION_TABLE_COLUMNS[0].name}) VALUES (?)`).run(version);
72+
};
73+
74+
/**
75+
* Migration from version 0 to version 1
76+
* - Adds attachments column to suites table
77+
*/
78+
const migrateV0ToV1 = (db: Database.Database): void => {
79+
debug('migrating from v0 to v1');
80+
db.prepare(`ALTER TABLE ${DB_SUITES_TABLE_NAME} ADD COLUMN ${DB_COLUMNS.ATTACHMENTS} TEXT DEFAULT '[]'`).run();
81+
82+
db.prepare(createTableQuery(DB_VERSION_TABLE_NAME, VERSION_TABLE_COLUMNS)).run();
83+
};
84+
85+
export const migrateDatabase = async (db: Database.Database, version: number): Promise<void> => {
86+
const migrations = [
87+
migrateV0ToV1
88+
];
89+
90+
try {
91+
if (version < 0 || version >= migrations.length) {
92+
throw new Error(`Unsupported database version encountered. Try deleting your report directory and restarting html-reporter.`);
93+
}
94+
95+
for (const migration of migrations.slice(version)) {
96+
migration(db);
97+
}
98+
99+
setDatabaseVersion(db, DB_CURRENT_VERSION);
100+
} catch (error) {
101+
debug(`Error during database migration: ${error}`);
102+
throw error;
103+
}
104+
};
105+
106+
export const backupAndReset = async (reportPath: string): Promise<string> => {
107+
const timestamp = Date.now();
108+
const backupDir = `${reportPath}_backup_${timestamp}`;
109+
debug(`Creating backup of corrupted database at: ${backupDir}`);
110+
111+
await fs.move(reportPath, backupDir);
112+
113+
return backupDir;
114+
};

lib/gui/tool-runner/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {ImagesInfoSaver} from '../../images-info-saver';
1717
import {SqliteImageStore} from '../../image-store';
1818
import * as reporterHelper from '../../reporter-helpers';
1919
import {logger, getShortMD5, isUpdatedStatus} from '../../common-utils';
20-
import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} from './utils';
20+
import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes, prepareLocalDatabase} from './utils';
2121
import {getExpectedCacheKey} from '../../server-utils';
2222
import {getTestsTreeFromDatabase} from '../../db-utils/server';
2323
import {
@@ -106,6 +106,7 @@ export class ToolRunner {
106106

107107
async initialize(): Promise<void> {
108108
await mergeDatabasesForReuse(this._reportPath);
109+
await prepareLocalDatabase(this._reportPath);
109110

110111
const dbClient = await SqliteClient.create({htmlReporter: this._toolAdapter.htmlReporter, reportPath: this._reportPath, reuse: true});
111112
const imageStore = new SqliteImageStore(dbClient);

lib/gui/tool-runner/utils.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ import Database from 'better-sqlite3';
66
import type {CoordBounds} from 'looks-same';
77

88
import {logger} from '../../common-utils';
9-
import {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} from '../../constants';
9+
import {DATABASE_URLS_JSON_NAME, DB_CURRENT_VERSION, LOCAL_DATABASE_NAME} from '../../constants';
1010
import {mergeTables} from '../../db-utils/server';
1111
import {TestEqualDiffsData, TestRefUpdateData} from '../../tests-tree-builder/gui';
1212
import {ImageInfoDiff, ImageSize} from '../../types';
13+
import {backupAndReset, getDatabaseVersion, migrateDatabase} from '../../db-utils/migrations';
14+
15+
import makeDebug from 'debug';
16+
17+
const debug = makeDebug('html-reporter:gui:tool-runner:utils');
1318

1419
export const formatId = (hash: string, browserId: string): string => `${hash}/${browserId}`;
1520

@@ -50,6 +55,36 @@ export const mergeDatabasesForReuse = async (reportPath: string): Promise<void>
5055
await Promise.all(dbPaths.map(p => fs.remove(p)));
5156
};
5257

58+
export const prepareLocalDatabase = async (reportPath: string): Promise<void> => {
59+
debug('prepareLocalDatabase', reportPath);
60+
const dbPath = path.resolve(reportPath, LOCAL_DATABASE_NAME);
61+
62+
if (!fs.existsSync(dbPath)) {
63+
return;
64+
}
65+
66+
const db = new Database(dbPath);
67+
try {
68+
const version = getDatabaseVersion(db);
69+
debug('determined db version', version);
70+
71+
if (version !== null && version < DB_CURRENT_VERSION) {
72+
await migrateDatabase(db, version);
73+
} else if (version === null) {
74+
const backupPath = await backupAndReset(reportPath);
75+
console.warn(`SQLite db at ${dbPath} is of unknown unsupported version.\nBacked up to ${backupPath} and starting from scratch.`);
76+
} else if (version > DB_CURRENT_VERSION) {
77+
const backupPath = await backupAndReset(reportPath);
78+
console.warn(`SQLite db at ${dbPath} is of unsupported version. ` +
79+
'This probably happened because the report was generated with a newer version of html-reporter than you are trying to use now. ' +
80+
'Please update html-reporter to the latest version to open this report.\n' +
81+
`Backed up to ${backupPath} and starting from scratch.`);
82+
}
83+
} finally {
84+
db.close();
85+
}
86+
};
87+
5388
export const filterByEqualDiffSizes = (imagesInfo: TestEqualDiffsData[], refDiffClusters?: CoordBounds[]): TestEqualDiffsData[] => {
5489
if (!refDiffClusters || _.isEmpty(refDiffClusters)) {
5590
return [];

lib/sqlite-client.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import fs from 'fs-extra';
55
import NestedError from 'nested-error-stacks';
66

77
import {getShortMD5} from './common-utils';
8-
import {TestStatus, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, LOCAL_DATABASE_NAME, DATABASE_URLS_JSON_NAME} from './constants';
8+
import {TestStatus, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, LOCAL_DATABASE_NAME, DATABASE_URLS_JSON_NAME, DB_CURRENT_VERSION} from './constants';
99
import {createTablesQuery} from './db-utils/common';
10+
import {setDatabaseVersion} from './db-utils/migrations';
1011
import type {Attachment, ImageInfoFull, TestError, TestStepCompressed} from './types';
1112
import {HtmlReporter} from './plugin-api';
1213
import {ReporterTestResult} from './adapters/test-result';
@@ -90,6 +91,10 @@ export class SqliteClient {
9091
debug('db connection opened');
9192

9293
createTablesQuery().forEach((query) => db?.prepare(query).run());
94+
95+
if (!options.reuse) {
96+
setDatabaseVersion(db, DB_CURRENT_VERSION);
97+
}
9398
} catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
9499
throw new NestedError(`Error creating database at "${dbPath}"`, err);
95100
}

lib/static/new-ui/experiments/time-travel/components/SnapshotsPlayer/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function SnapshotsPlayer(): ReactNode {
7272
const [playerWidth, setPlayerWidth] = useState<number>(1200);
7373
const [playerHeight, setPlayerHeight] = useState<number>(800);
7474
const isLiveMaxSizeInitialized = useRef(false);
75-
const [maxPlayerSize, setMaxPlayerSize] = useState<ImageSize>({height: 0, width: 0});
75+
const [maxPlayerSize, setMaxPlayerSize] = useState<ImageSize>({height: 400, width: 700});
7676

7777
const [totalTime, setTotalTime] = useState(0);
7878
const [isPlaying, setIsPlaying] = useState(false);

lib/static/new-ui/experiments/time-travel/components/TestInfo/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export function TestInfo(): ReactNode {
1717
const currentResult = useSelector(getCurrentResult);
1818
const browserFeatures = useSelector(state => state.browserFeatures);
1919

20-
const isSnapshotAvailable = currentResult?.attachments?.some(attachment => attachment.type === AttachmentType.Snapshot);
21-
const isLiveStreamingAvailable = browserFeatures[currentResult?.name ?? '']?.some(feature => feature === BrowserFeature.LiveSnapshotsStreaming);
22-
const shouldShowPlayer = isSnapshotAvailable || isLiveStreamingAvailable;
23-
2420
const steps = useSelector(getTestSteps);
2521
const isRunning = useSelector(state => state.running);
2622

23+
const isSnapshotAvailable = currentResult?.attachments?.some(attachment => attachment.type === AttachmentType.Snapshot);
24+
const isLiveStreamingAvailable = browserFeatures[currentResult?.name ?? '']?.some(feature => feature === BrowserFeature.LiveSnapshotsStreaming);
25+
const shouldShowPlayer = isSnapshotAvailable || (isRunning && isLiveStreamingAvailable);
26+
2727
return <>
2828
<CollapsibleSection id={'actions'} title={'Actions'}>
2929
<div className={styles.stepsContainer}>

0 commit comments

Comments
 (0)