diff --git a/.github/workflows/check_oss_submodule.yml b/.github/workflows/check_oss_submodule.yml new file mode 100644 index 000000000..6d51856b0 --- /dev/null +++ b/.github/workflows/check_oss_submodule.yml @@ -0,0 +1,20 @@ +name: Check OSS Submodule + +on: + push: + +jobs: + check_oss_submodule: + name: Check if OSS is on master + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: "recursive" + + - name: Check OSS branch + run: | + python3 -m pip install -U pip + python3 -m pip install --upgrade pygit2==1.14.1 + python3 submodule_is_mergeable.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f04c4a1a2..a0f362657 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,16 +7,30 @@ jobs: CI: name: Monorepo CI flow runs-on: ubuntu-latest - services: - questdb: - image: questdb/questdb:nightly - ports: - - 9000:9000 - env: - QDB_TELEMETRY_ENABLED: false steps: - uses: actions/checkout@v4 + with: + submodules: "recursive" + + - uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Build QuestDB + run: mvn clean package -f packages/browser-tests/questdb/pom.xml -DskipTests -P build-binaries + + - name: Extract QuestDB + run: tar -xzf packages/browser-tests/questdb/core/target/questdb-*-rt-linux-amd64.tar.gz -C tmp/ + + - name: Create DB Root + run: mkdir tmp/dbroot + + - name: Start QuestDB + run: ./tmp/questdb-*-rt-linux-amd64/bin/questdb.sh start -d ./tmp/dbroot + - uses: actions/setup-node@v4 with: node-version: "20" @@ -40,6 +54,21 @@ jobs: - name: Run browser-tests test run: node packages/web-console/serve-dist.js & yarn workspace browser-tests test + - name: Print Log Files + if: success() || failure() + run: cat tmp/dbroot/log/* + + - name: Save QuestDB log on test failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: questdb-log + path: tmp/dbroot/log/* + + - name: Stop QuestDB + if: success() || failure() + run: ./tmp/questdb-*-rt-linux-amd64/bin/questdb.sh stop + - name: Publish @questdb/web-console to npm if: github.ref == 'refs/heads/main' uses: JS-DevTools/npm-publish@v1 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..7f2bb490b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "questdb"] + path = packages/browser-tests/questdb + url = https://github.com/questdb/questdb.git diff --git a/.pnp.cjs b/.pnp.cjs index f867b767a..f6683d490 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -13597,7 +13597,8 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["browser-tests", "workspace:packages/browser-tests"],\ ["@simonsmith/cypress-image-snapshot", "virtual:eeb440888f0c831c010bf83e71c16458314f99874c6109e2303fb2248287f56d6750dd5ddd598c74aba008850cb93bae79351d816a9905f40e024f392c8ad6b5#npm:8.1.2"],\ - ["cypress", "npm:10.11.0"]\ + ["cypress", "npm:10.11.0"],\ + ["cypress-real-events", "virtual:eeb440888f0c831c010bf83e71c16458314f99874c6109e2303fb2248287f56d6750dd5ddd598c74aba008850cb93bae79351d816a9905f40e024f392c8ad6b5#npm:1.13.0"]\ ],\ "linkType": "SOFT"\ }]\ @@ -15081,6 +15082,28 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["cypress-real-events", [\ + ["npm:1.13.0", {\ + "packageLocation": "./.yarn/cache/cypress-real-events-npm-1.13.0-d74f2d882d-085e2e4f45.zip/node_modules/cypress-real-events/",\ + "packageDependencies": [\ + ["cypress-real-events", "npm:1.13.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:eeb440888f0c831c010bf83e71c16458314f99874c6109e2303fb2248287f56d6750dd5ddd598c74aba008850cb93bae79351d816a9905f40e024f392c8ad6b5#npm:1.13.0", {\ + "packageLocation": "./.yarn/__virtual__/cypress-real-events-virtual-07869643c0/0/cache/cypress-real-events-npm-1.13.0-d74f2d882d-085e2e4f45.zip/node_modules/cypress-real-events/",\ + "packageDependencies": [\ + ["cypress-real-events", "virtual:eeb440888f0c831c010bf83e71c16458314f99874c6109e2303fb2248287f56d6750dd5ddd598c74aba008850cb93bae79351d816a9905f40e024f392c8ad6b5#npm:1.13.0"],\ + ["@types/cypress", null],\ + ["cypress", "npm:10.11.0"]\ + ],\ + "packagePeers": [\ + "@types/cypress",\ + "cypress"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["dashdash", [\ ["npm:1.14.1", {\ "packageLocation": "./.yarn/cache/dashdash-npm-1.14.1-be8f10a286-137b287fa0.zip/node_modules/dashdash/",\ diff --git a/.yarn/cache/cypress-real-events-npm-1.13.0-d74f2d882d-085e2e4f45.zip b/.yarn/cache/cypress-real-events-npm-1.13.0-d74f2d882d-085e2e4f45.zip new file mode 100644 index 000000000..ecf15239a Binary files /dev/null and b/.yarn/cache/cypress-real-events-npm-1.13.0-d74f2d882d-085e2e4f45.zip differ diff --git a/packages/browser-tests/cypress.config.js b/packages/browser-tests/cypress.config.js index 3b2d0ba37..ff336654d 100644 --- a/packages/browser-tests/cypress.config.js +++ b/packages/browser-tests/cypress.config.js @@ -5,7 +5,7 @@ const { module.exports = defineConfig({ e2e: { - defaultCommandTimeout: 10000, + defaultCommandTimeout: 30000, screenshotOnRunFailure: false, video: false, baseUrl: "http://localhost:9999", diff --git a/packages/browser-tests/cypress/commands.js b/packages/browser-tests/cypress/commands.js index fcd3d6760..b5091713a 100644 --- a/packages/browser-tests/cypress/commands.js +++ b/packages/browser-tests/cypress/commands.js @@ -2,6 +2,8 @@ const { addMatchImageSnapshotCommand, } = require("@simonsmith/cypress-image-snapshot/command"); +require("cypress-real-events"); + addMatchImageSnapshotCommand({ failureThreshold: 0.3, blackout: [".notifications", 'button[class*="BuildVersion"'], @@ -24,13 +26,23 @@ beforeEach(() => { cy.intercept( { method: "GET", - url: "/**", - hostname: "api.github.com", + url: "/github/latest", + hostname: "github-api.questdb.io", }, (req) => { req.reply("{}"); } ); + cy.intercept( + { + method: "GET", + url: "/news", + hostname: "cloud.questdb.com", + }, + (req) => { + req.reply("[]"); + } + ); }); Cypress.Commands.add("getGrid", () => @@ -50,7 +62,7 @@ Cypress.Commands.add("getGridCol", (n) => Cypress.Commands.add("getGridRows", () => cy.get(".qg-r").filter(":visible")); Cypress.Commands.add("typeQuery", (query) => - cy.getEditor().click({ force: true }).type(query) + cy.getEditor().realClick().type(query) ); Cypress.Commands.add("runLine", () => { @@ -77,6 +89,10 @@ Cypress.Commands.add("selectQuery", (n) => .click() ); +Cypress.Commands.add("getMountedEditor", () => + cy.get(".monaco-scrollable-element") +); + Cypress.Commands.add("getEditor", () => cy.get(".monaco-editor[role='code'] ")); Cypress.Commands.add("getEditorContent", () => @@ -116,13 +132,7 @@ Cypress.Commands.add("matchErrorMarkerPosition", ({ left, width }) => Cypress.Commands.add("F9", () => { cy.intercept("/exec*").as("exec"); - return cy - .getEditor() - .trigger("keydown", { - keyCode: 120, - }) - .wait("@exec") - .wait(501); + return cy.getEditor().realPress("F9").wait("@exec").wait(501); }); Cypress.Commands.add("getSelectedLines", () => cy.get(".selected-text")); diff --git a/packages/browser-tests/cypress/integration/console/editor.spec.js b/packages/browser-tests/cypress/integration/console/editor.spec.js index 6baa7cd46..d303e2e7c 100644 --- a/packages/browser-tests/cypress/integration/console/editor.spec.js +++ b/packages/browser-tests/cypress/integration/console/editor.spec.js @@ -5,11 +5,11 @@ const baseUrl = "http://localhost:9999"; describe("appendQuery", () => { const consoleConfiguration = { savedQueries: [ - { name: "query 1", value: "first query" }, - { name: "query 1", value: "second query" }, + { name: "query 1", value: "first query;" }, + { name: "query 2", value: "second query;" }, { - name: "query 1", - value: "multi\nline\nquery", + name: "query 3", + value: "multi\nline\nquery;", }, ], }; @@ -26,16 +26,17 @@ describe("appendQuery", () => { ).as("getConsoleConfiguration"); cy.visit(baseUrl); - cy.getEditor().should("be.visible"); + cy.getEditorContent().should("be.visible"); }); - afterEach(() => { + beforeEach(() => { + cy.getEditorContent().should("be.visible"); cy.clearEditor(); }); it("should append and select first query", () => { cy.selectQuery(0); - const expected = `${queries[0]}\n`; + const expected = `\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); cy.matchImageSnapshot(); // screenshot diff @@ -43,14 +44,14 @@ describe("appendQuery", () => { it("should append and select second query", () => { cy.selectQuery(1); - const expected = `${queries[1]}\n`; + const expected = `\n${queries[1]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); it("should append and select multiline query", () => { cy.selectQuery(2); - const expected = `${queries[2]}\n`; + const expected = `\n${queries[2]}`; cy.getEditorContent().should("have.value", expected); // monaco editor visually selects all 3 lines, but creates 4 elements to visualise selection cy.getSelectedLines().should("have.length", 4); @@ -58,19 +59,19 @@ describe("appendQuery", () => { it("should correctly append and select query after multiple inserts", () => { cy.selectQuery(1); - cy.selectQuery(2); - cy.typeQuery(`{ctrl}g2{enter}`); // go to line 2 cy.selectQuery(1); - const expected = `${queries[1]}\n\n${queries[1]}\n\n${queries[2]}\n`; + cy.typeQuery(`{ctrl}g2{enter}`); // go to line 2 + cy.selectQuery(2); + const expected = `\n${queries[1]}\n\n${queries[1]}\n\n${queries[2]}`; cy.getEditorContent().should("have.value", expected); - cy.getSelectedLines().should("have.length", 1); + cy.getSelectedLines().should("have.length", 4); }); it("should correctly append and select query when position is first line which is empty", () => { cy.typeQuery(`{enter}--b{upArrow}`); cy.selectQuery(0); cy.selectQuery(1); - const expected = `${queries[0]}\n\n${queries[1]}\n\n--b`; + const expected = `\n--b\n${queries[0]}\n\n${queries[1]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); @@ -78,7 +79,7 @@ describe("appendQuery", () => { it("should correctly append and select query when position is first line which is not empty", () => { cy.typeQuery(`--a`); cy.selectQuery(0); - const expected = `--a\n\n${queries[0]}\n`; + const expected = `--a\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); @@ -86,7 +87,7 @@ describe("appendQuery", () => { it("should correctly append and select query when position is first line which is not empty and there's more content after", () => { cy.typeQuery(`--a{enter}{enter}--b{upArrow}{upArrow}`); cy.selectQuery(0); - const expected = `--a\n\n${queries[0]}\n\n--b`; + const expected = `--a\n\n--b\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); @@ -94,7 +95,7 @@ describe("appendQuery", () => { it("should correctly append and add surrounding new lines when position is middle line which is empty", () => { cy.typeQuery(`--a{enter}{enter}--b{upArrow}`); cy.selectQuery(0); - const expected = `--a\n\n${queries[0]}\n\n--b`; + const expected = `--a\n\n--b\n\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); @@ -102,7 +103,7 @@ describe("appendQuery", () => { it("should correctly append and add surrounding new lines when position is last line which is empty", () => { cy.typeQuery(`--a{enter}--b`); cy.selectQuery(0); - const expected = `--a\n--b\n\n${queries[0]}\n`; + const expected = `--a\n--b\n\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); cy.matchImageSnapshot(); @@ -111,7 +112,7 @@ describe("appendQuery", () => { it("should correctly append and add surrounding new lines when there are two lines and position is last line which is empty", () => { cy.typeQuery(`--a{enter}`); cy.selectQuery(0); - const expected = `--a\n\n${queries[0]}\n`; + const expected = `--a\n\n\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); @@ -120,28 +121,29 @@ describe("appendQuery", () => { cy.typeQuery(`--a{enter}--b{enter}{enter}--c`); cy.typeQuery(`{ctrl}g2{enter}{rightArrow}`); // go to line 2 cy.selectQuery(0); - const expected = `--a\n--b\n\n${queries[0]}\n\n--c`; + const expected = `--a\n--b\n\n--c\n\n${queries[0]}`; cy.getEditorContent().should("have.value", expected); cy.getSelectedLines().should("have.length", 1); }); }); describe("&query URL param", () => { - afterEach(() => { + beforeEach(() => { + cy.visit(baseUrl); + cy.getEditorContent().should("be.visible"); cy.clearEditor(); }); it("should append and select single line query", () => { - cy.visit(baseUrl); cy.typeQuery("select x from long_sequence(1)"); // running query caches it, it's available after refresh const query = encodeURIComponent("select x+1 from long_sequence(1)"); cy.visit(`${baseUrl}/?query=${query}&executeQuery=true`); + cy.getEditorContent().should("be.visible"); cy.getGridRow(0).should("contain", "2"); cy.getSelectedLines().should("have.length", 1); }); it("should append and select multiline query", () => { - cy.visit(baseUrl); cy.typeQuery( `select x\nfrom long_sequence(1);\n\n-- a\n-- b\n-- c\n${"{upArrow}".repeat( 5 @@ -149,29 +151,26 @@ describe("&query URL param", () => { ); const query = encodeURIComponent("select x+1\nfrom\nlong_sequence(1);"); cy.visit(`${baseUrl}?query=${query}&executeQuery=true`); + cy.getEditorContent().should("be.visible"); cy.getGridRow(0).should("contain", "2"); cy.getSelectedLines().should("have.length", 4); }); - it.skip("should not append query if it already exists in editor", () => { - cy.visit(baseUrl); + it("should not append query if it already exists in editor", () => { const query = "select x\nfrom long_sequence(1);\n\n-- a\n-- b\n-- c"; cy.typeQuery(query).clickRun(); cy.visit(`${baseUrl}?query=${encodeURIComponent(query)}&executeQuery=true`); + cy.getEditorContent().should("be.visible"); cy.getEditorContent().should("have.value", query); }); it("should append query and scroll to it", () => { - cy.visit(baseUrl); - - cy.typeQuery("--\n".repeat(20)); // take space so that query is not visible later - const query = "select x from long_sequence(1);"; - cy.typeQuery(query).clickRun(); // save by running + cy.typeQuery("select x from long_sequence(1);"); + cy.typeQuery("\n".repeat(20)).clickRun(); // take space so that query is not visible later, save by running const appendedQuery = "-- hello world"; - cy.visit( - `${baseUrl}?query=${encodeURIComponent(appendedQuery)}&executeQuery=true` - ); + cy.visit(`${baseUrl}?query=${encodeURIComponent(appendedQuery)}`); + cy.getEditorContent().should("be.visible"); cy.getVisibleLines() .invoke("text") .should("match", /hello.world$/); // not matching on appendedQuery, because query should be selected for which Monaco adds special chars between words @@ -180,15 +179,29 @@ describe("&query URL param", () => { describe("autocomplete", () => { before(() => { - cy.visit(baseUrl); + cy.getEditorContent().should("be.visible"); + ["my_secrets", "my_secrets2", "my_publics"].forEach((table) => { + cy.typeQuery(`drop table if exists "${table}"`).runLine().clearEditor(); + }); + [ + 'create table "my_publics" ("public" string);', + // We're creating another table with the same column name. + // The autocomplete should merge the column completions into one + // and respond with something like `secret (my_secrets, my_secrets2)` + 'create table "my_secrets" ("secret" string);', + 'create table "my_secrets2" ("secret" string);', + ].forEach((query) => { + cy.typeQuery(query).runLine().clearEditor(); + }); }); beforeEach(() => { - cy.getEditor().should("be.visible"); + cy.visit(baseUrl); + cy.getEditorContent().should("be.visible"); cy.clearEditor(); }); - it("should work when tables list is empty", () => { + it("should work when provided table name doesn't exist", () => { cy.typeQuery("select * from teletubies") .getAutocomplete() .should("not.be.visible") @@ -197,29 +210,19 @@ describe("autocomplete", () => { cy.matchImageSnapshot(); }); - it("should work when tables list is not empty", () => { - cy.typeQuery('create table "my_secrets" ("secret" string);') - .clickRun() - .clearEditor(); - - // We're creating another table with the same column name. - // The autocomplete should merge the column completions into one - // and respond with something like `secret (my_secrets, my_secrets2)` - cy.typeQuery('create table "my_secrets2" ("secret" string);') - .clickRun() - .clearEditor(); - - cy.typeQuery('create table "my_publics" ("public" string);') - .clickRun() - .clearEditor(); - - cy.visit(baseUrl); - cy.typeQuery("\nselect "); + it("should suggest the existing tables on 'from' clause", () => { + cy.typeQuery("select * from "); cy.getAutocomplete() - // Tables + // tables .should("not.contain", "telemetry") .should("contain", "my_secrets") .should("contain", "my_publics") + .clearEditor(); + }); + + it("should suggest columns and tables on 'select' clause", () => { + cy.typeQuery("select "); + cy.getAutocomplete() // Columns .should("contain", "secret") .should("contain", "public") @@ -227,22 +230,22 @@ describe("autocomplete", () => { // list the tables containing `secret` column .should("contain", "my_secrets, my_secrets2") .clearEditor(); + }); + it("should suggest correct columns on 'where' filter", () => { cy.typeQuery("select * from my_secrets where "); cy.getAutocomplete() .should("contain", "secret") .should("not.contain", "public") .clearEditor(); + }); + it("should suggest correct columns on 'on' clause", () => { cy.typeQuery("select * from my_secrets join my_publics on "); cy.getAutocomplete() .should("contain", "my_publics.public") .should("contain", "my_secrets.secret") .clearEditor(); - - cy.typeQuery('drop table "my_secrets"').runLine().clearEditor(); - cy.typeQuery('drop table "my_secrets2"').runLine().clearEditor(); - cy.typeQuery('drop table "my_publics"').runLine().clearEditor(); }); }); @@ -251,7 +254,8 @@ describe("errors", () => { cy.visit(baseUrl); }); - afterEach(() => { + beforeEach(() => { + cy.getEditorContent().should("be.visible"); cy.clearEditor(); }); @@ -271,12 +275,13 @@ describe("errors", () => { }); }); -describe.skip("running query with F9", () => { +describe("running query with F9", () => { before(() => { cy.visit(baseUrl); }); - afterEach(() => { + beforeEach(() => { + cy.getEditorContent().should("be.visible"); cy.clearEditor(); }); diff --git a/packages/browser-tests/package.json b/packages/browser-tests/package.json index a36b6fd4b..756a6b8a5 100644 --- a/packages/browser-tests/package.json +++ b/packages/browser-tests/package.json @@ -9,5 +9,8 @@ "devDependencies": { "@simonsmith/cypress-image-snapshot": "^8.0.1", "cypress": "^10.11.0" + }, + "dependencies": { + "cypress-real-events": "^1.13.0" } } diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb new file mode 160000 index 000000000..03345738a --- /dev/null +++ b/packages/browser-tests/questdb @@ -0,0 +1 @@ +Subproject commit 03345738a1bbcc0689ea2c56742a03abc4220182 diff --git a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts index 675334395..0de5ec4dc 100644 --- a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts +++ b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts @@ -23,8 +23,23 @@ export const createSchemaCompletionProvider = ( const queryAtCursor = getQueryFromCursor(editor) + // get text value in the current line + const textInLine = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column, + }) + let tableContext: string[] = [] + const isWhitespaceOnly = /^\s*$/.test(textInLine) + const isLineComment = /(-- |--|\/\/ |\/\/)$/gim.test(textInLine) + + if (isWhitespaceOnly || isLineComment) { + return null + } + if (queryAtCursor) { const matches = findMatches(model, queryAtCursor.query) if (matches.length > 0) { @@ -79,16 +94,13 @@ export const createSchemaCompletionProvider = ( endLineNumber: position.lineNumber, endColumn: position.column, }) - // check if `textInLine` contains whitespaces only - const isWhitespaceOnly = /^\s*$/.test(textInLine) if ( - (/(FROM|INTO|(ALTER|BACKUP|DROP|REINDEX|RENAME|TRUNCATE|VACUUM) TABLE|JOIN|UPDATE)\s$/gim.test( + /(FROM|INTO|(ALTER|BACKUP|DROP|REINDEX|RENAME|TRUNCATE|VACUUM) TABLE|JOIN|UPDATE)\s$/gim.test( textUntilPosition, ) || - (/'$/gim.test(textUntilPosition) && - !textUntilPosition.endsWith("= '"))) && - !isWhitespaceOnly + (/'$/gim.test(textUntilPosition) && + !textUntilPosition.endsWith("= '")) ) { return { suggestions: getTableCompletions({ diff --git a/packages/web-console/src/scenes/Editor/QueryPicker/index.tsx b/packages/web-console/src/scenes/Editor/QueryPicker/index.tsx index 182e6d944..7ba409ffc 100644 --- a/packages/web-console/src/scenes/Editor/QueryPicker/index.tsx +++ b/packages/web-console/src/scenes/Editor/QueryPicker/index.tsx @@ -131,7 +131,7 @@ const QueryPicker = ({ hidePicker, queries, ref }: Props) => { const addQuery = useCallback( (query: Query) => { hidePicker() - appendQuery(query.value) + appendQuery(query.value, { appendAt: "end" }) }, [hidePicker], ) diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..d91800636 --- /dev/null +++ b/pom.xml @@ -0,0 +1,13 @@ + + + + 4.0.0 + + ui + ui + 1.0-SNAPSHOT + + ui + https://questdb.io + diff --git a/run_browser_tests.sh b/run_browser_tests.sh new file mode 100755 index 000000000..8c51ce570 --- /dev/null +++ b/run_browser_tests.sh @@ -0,0 +1,35 @@ +#!/bin/bash -x + +# Run it from the 'ui' directory as: +# ./run_browser_tests.sh + +# You can also run it without building the QuestDB project (if it is already built): +# ./run_browser_tests.sh -skipQuestDBBuild + +rm -rf packages/browser-tests/cypress/snapshots/* +rm -rf tmp/questdb-* +rm -rf tmp/dbroot + +if [[ $1 = "-skipQuestDBBuild" ]] +then + echo "Skipping QuestDB build" +else + mvn clean package -f packages/browser-tests/questdb/pom.xml -DskipTests -P build-binaries +fi + +tar xzf packages/browser-tests/questdb/core/target/questdb-*-rt-*.tar.gz -C tmp/ +mkdir tmp/dbroot + +yarn install --immutable --immutable-cache +yarn workspace @questdb/react-components run build +yarn workspace @questdb/web-console run build + +./tmp/questdb-*/bin/questdb.sh start -d ./tmp/dbroot +node packages/web-console/serve-dist.js & +PID="$!" +echo "Proxy started, PID=$PID" + +yarn workspace browser-tests test + +kill -SIGTERM $PID +./tmp/questdb-*/bin/questdb.sh stop diff --git a/submodule_is_mergeable.py b/submodule_is_mergeable.py new file mode 100755 index 000000000..f77636424 --- /dev/null +++ b/submodule_is_mergeable.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +""" +Ensure that the PR's `questdb` submodule is mergeable. + +================================================ +This script does not modify the repo in any way. +================================================ + +This is achieved if the commit that the submodule points to respects that it: +* Is a commit that is in the `questdb` master branch's history. +* Is no more than ACCEPTABLE_LAG commits behind. +* Is same or newer as the current commit pointed to currently. + +Before running, you may first need to install dependencies: + + $ python3 -m pip install -U pip + $ python3 -m pip install -U pygit2 + +To run, from the root of the repo, call: + + git fetch + (cd questdb && git fetch) + python3 ci/submodule_is_mergeable.py + +On error, will return non-zero exit code and fail the CI run. +""" + +import sys +sys.dont_write_bytecode = True +import pygit2 + + +# Number of commits that the submodule can be behind the OSS master branch +# If it's older than this, it really ought to be updated. +ACCEPTABLE_LAG = 5 + + +def get_last_master_commits(questdb_repo: pygit2.Repository) -> [pygit2.Oid]: + commit_ids = [] + oss_master_ref = questdb_repo.references['refs/remotes/origin/master'] + origin_master_head: pygit2.Oid = oss_master_ref.peel().id + for index, commit in enumerate(questdb_repo.walk(origin_master_head)): + # print(f'{commit.id}\t{commit.message}') + commit_ids.append(commit.id) + if index > ACCEPTABLE_LAG: + break + return commit_ids + + +def check_commit_is_from_recent_master(oss_master_commits: [pygit2.Oid], oss_head: pygit2.Oid): + if oss_head not in oss_master_commits: + sys.stderr.write('The `questdb` submodule is not mergeable.\n') + sys.stderr.write( + f'The submodule\'s commit ({oss_head}) is not one of the ' + + f'last {ACCEPTABLE_LAG} commits in the `questdb` master branch.\n') + for commit in oss_master_commits: + sys.stderr.write(f' * {commit}\n') + sys.exit(1) + + +def check_not_older( + ent_repo: pygit2.Repository, + oss_master_commits: [pygit2.Oid], + oss_head: pygit2.Oid): + ent_main_head = ent_repo.references['refs/remotes/origin/main'].peel().id + ent_main_tree = ent_repo[ent_main_head].tree + questdb_main_commit_id = ent_main_tree['questdb'].id + + if questdb_main_commit_id not in oss_master_commits: + sys.stderr.write( + 'The submodule pointed to by `main` is not in the ' + + f'last {ACCEPTABLE_LAG} commits from the `questdb` master branch.\n') + return + + old_index = oss_master_commits.index(questdb_main_commit_id) + new_index = oss_master_commits.index(oss_head) + if new_index > old_index: + sys.stderr.write('The `questdb` submodule is not mergeable.\n') + sys.stderr.write( + f'This branch\'s `questdb` submodule\'s commit is older than ' + + f'the commit pointed to by the `main` branch.\n') + sys.stderr.write( + f' * This branch points to: {oss_head}\n' + + f' * The `main` branch to: {questdb_main_commit_id}\n') + sys.exit(1) + + +def main(): + # Open up the enterprise and submodule repos. + ent_repo = pygit2.Repository('.git') + questdb_repo: pygit2.Repository = ent_repo.lookup_submodule('questdb').open() + + # The current commit id pointed to by the submodule in this branch. + oss_head: pygit2.Oid = questdb_repo.head.target + + # The last few commits in the `questdb` origin/master branch. + oss_master_commits = get_last_master_commits(questdb_repo) + + # Check that the submodule's commit is from that list. + check_commit_is_from_recent_master(oss_master_commits, oss_head) + + # Check that the submodule's commit is not older than the current commit + # from the `main` branch of the enterprise repo. + # check_not_older(ent_repo, oss_master_commits, oss_head) + + +if __name__ == '__main__': + main() diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/yarn.lock b/yarn.lock index 1d695132b..680339df6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7989,6 +7989,7 @@ __metadata: dependencies: "@simonsmith/cypress-image-snapshot": "npm:^8.0.1" cypress: "npm:^10.11.0" + cypress-real-events: "npm:^1.13.0" languageName: unknown linkType: soft @@ -9233,6 +9234,15 @@ __metadata: languageName: node linkType: hard +"cypress-real-events@npm:^1.13.0": + version: 1.13.0 + resolution: "cypress-real-events@npm:1.13.0" + peerDependencies: + cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x + checksum: 10/085e2e4f456fdf4173fd07fc2602ce44d661256628a5b40e89092e9792533f46b808f97106d9f9ea0272279b5c10db4eef3f0606e3705d3638d552d0999425de + languageName: node + linkType: hard + "cypress@npm:^10.11.0": version: 10.11.0 resolution: "cypress@npm:10.11.0"