diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..68933e7f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: gristlabs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5b5190e6..0ad6ad10 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,24 +10,35 @@ on: workflow_dispatch: jobs: - build: + build_and_test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9] node-version: [14.x] + tests: + - ':lint:python:client:common:smoke:' + - ':server-1-of-2:' + - ':server-2-of-2:' + - ':nbrowser-1-of-5:' + - ':nbrowser-2-of-5:' + - ':nbrowser-3-of-5:' + - ':nbrowser-4-of-5:' + - ':nbrowser-5-of-5:' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: 'yarn' - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: 'pip' - name: Install Python packages run: | @@ -38,9 +49,11 @@ jobs: run: yarn install - name: Run eslint + if: contains(matrix.tests, ':lint:') run: yarn run lint:ci - name: Make sure bucket is versioned + if: contains(matrix.tests, ':server-') env: AWS_ACCESS_KEY_ID: administrator AWS_SECRET_ACCESS_KEY: administrator @@ -49,15 +62,27 @@ jobs: - name: Build Node.js code run: yarn run build:prod - - name: Run smoke test + if: contains(matrix.tests, ':smoke:') run: VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:smoke - name: Run python tests + if: contains(matrix.tests, ':python:') run: yarn run test:python + - name: Run client tests + if: contains(matrix.tests, ':client:') + run: yarn run test:client + + - name: Run common tests + if: contains(matrix.tests, ':common:') + run: yarn run test:common + - name: Run server tests with minio and redis - run: MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:server + if: contains(matrix.tests, ':server-') + run: | + export TEST_SPLITS=$(echo $TESTS | sed "s/.*:server-\([^:]*\).*/\1/") + MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:server env: GRIST_DOCS_MINIO_ACCESS_KEY: administrator GRIST_DOCS_MINIO_SECRET_KEY: administrator @@ -68,15 +93,12 @@ jobs: GRIST_DOCS_MINIO_BUCKET: grist-docs-test - name: Run main tests without minio and redis - run: MOCHA_WEBDRIVER_HEADLESS=1 yarn run test --exclude '_build/test/server/**/*' - - - name: Update candidate branch - if: ${{ github.event_name == 'push' }} - uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: latest_candidate - force: true + if: contains(matrix.tests, ':nbrowser-') + run: | + export TEST_SPLITS=$(echo $TESTS | sed "s/.*:nbrowser-\([^:]*\).*/\1/") + MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:nbrowser + env: + TESTS: ${{ matrix.tests }} services: # https://github.com/bitnami/bitnami-docker-minio/issues/16 @@ -103,3 +125,18 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + + candidate: + needs: build_and_test + if: ${{ success() && github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Fetch new candidate branch + uses: actions/checkout@v3 + + - name: Update candidate branch + uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: latest_candidate + force: true diff --git a/app/client/ui/VisibleFieldsConfig.ts b/app/client/ui/VisibleFieldsConfig.ts index c52e543a..4222fd8d 100644 --- a/app/client/ui/VisibleFieldsConfig.ts +++ b/app/client/ui/VisibleFieldsConfig.ts @@ -198,7 +198,7 @@ export class VisibleFieldsConfig extends Disposable { }); return [ cssHeader( - cssFieldListHeader(dom.text((use) => `Visible ${use(this._fieldLabel)}`)), + cssFieldListHeader(dom.text((use) => t("Visible {{label}}", {label: use(this._fieldLabel)}))), dom.maybe( (use) => Boolean(use(use(this._section.viewFields).getObservable()).length), () => ( @@ -215,7 +215,7 @@ export class VisibleFieldsConfig extends Disposable { dom.maybe(this._showVisibleBatchButtons, () => cssRow( primaryButton( - dom.text((use) => `Hide ${use(this._fieldLabel)}`), + dom.text((use) => t("Hide {{label}}", {label: use(this._fieldLabel)})), dom.on('click', () => this._removeSelectedFields()), ), basicButton( @@ -234,7 +234,7 @@ export class VisibleFieldsConfig extends Disposable { testId('collapse-hidden'), ), // TODO: show `hidden column` only when some fields are hidden - cssFieldListHeader(dom.text((use) => `Hidden ${use(this._fieldLabel)}`)), + cssFieldListHeader(dom.text((use) => t("Hidden {{label}}", {label: use(this._fieldLabel)}))), dom.maybe( (use) => Boolean(use(this._hiddenFields.getObservable()).length && !use(this._collapseHiddenFields)), () => ( @@ -257,7 +257,7 @@ export class VisibleFieldsConfig extends Disposable { dom.maybe(this._showHiddenBatchButtons, () => cssRow( primaryButton( - dom.text((use) => `Show ${use(this._fieldLabel)}`), + dom.text((use) => t("Show {{label}}", {label: use(this._fieldLabel)})), dom.on('click', () => this._addSelectedFields()), ), basicButton( diff --git a/app/client/ui2018/menus.ts b/app/client/ui2018/menus.ts index 93fe952f..a3efd59a 100644 --- a/app/client/ui2018/menus.ts +++ b/app/client/ui2018/menus.ts @@ -150,7 +150,7 @@ export function select(obs: Observable, optionArray: MaybeObsArray cssOptionRow( op.icon ? cssOptionRowIcon(op.icon) : null, - cssOptionLabel(op.label), + cssOptionLabel(t(op.label)), renderOptionArgs ? renderOptionArgs(op) : null, testId('select-row') ) diff --git a/package.json b/package.json index 2fcdc261..66062748 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grist-core", - "version": "1.0.7", + "version": "1.0.8", "license": "Apache-2.0", "description": "Grist is the evolution of spreadsheets", "homepage": "https://github.com/gristlabs/grist-core", @@ -13,10 +13,10 @@ "build:prod": "buildtools/build.sh", "start:prod": "sandbox/run.sh", "test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+-b --no-exit} --slow 8000 ${DEBUG:---forbid-only} -g ${GREP_TESTS:-''} '_build/test/common/*.js' '_build/test/client/*.js' '_build/test/nbrowser/*.js' '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", - "test:nbrowser": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+-b --no-exit} ${DEBUG:---forbid-only} -g ${GREP_TESTS:-''} --slow 8000 '_build/test/nbrowser/**/*.js'", + "test:nbrowser": "TEST_SUITE=nbrowser TEST_SUITE_FOR_TIMINGS=nbrowser TIMINGS_FILE=test/timings/nbrowser.txt GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+-b --no-exit} ${DEBUG:---forbid-only} -g ${GREP_TESTS:-''} --slow 8000 -R test/xunit-file '_build/test/nbrowser/**/*.js'", "test:client": "GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'", "test:common": "GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'", - "test:server": "GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", + "test:server": "TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", "test:smoke": "NODE_PATH=_build:_build/stubs:_build/ext mocha _build/test/nbrowser/Smoke.js", "test:docker": "./test/test_under_docker.sh", "test:python": "sandbox_venv3/bin/python sandbox/grist/runtests.py ${GREP_TESTS:+discover -p \"test*${GREP_TESTS}*.py\"}", diff --git a/static/locales/de.client.json b/static/locales/de.client.json index 60a5e81d..a66521e2 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -744,7 +744,11 @@ "Cannot drop items into Hidden Fields": "Elemente können nicht in ausgeblendeten Feldern abgelegt werden", "Clear": "Klären", "Hidden Fields cannot be reordered": "Ausgeblendete Felder können nicht neu sortiert werden", - "Select All": "Alle auswählen" + "Select All": "Alle auswählen", + "Hide {{label}}": "{{label}} ausblenden", + "Hidden {{label}}": "{{label}} Versteckt", + "Show {{label}}": "{{label}} anzeigen", + "Visible {{label}}": "Sichtbar {{label}}" }, "WelcomeQuestions": { "Education": "Bildung", @@ -803,7 +807,19 @@ "menus": { "* Workspaces are available on team plans. ": "* Arbeitsbereiche sind in Teamplänen verfügbar. ", "Select fields": "Felder auswählen", - "Upgrade now": "Jetzt aktualisieren" + "Upgrade now": "Jetzt aktualisieren", + "Numeric": "Numerisch", + "DateTime": "DatumUhrzeit", + "Choice List": "Auswahlliste", + "Choice": "Auswahl", + "Reference": "Referenz", + "Reference List": "Referenzliste", + "Attachment": "Anhang", + "Any": "Jegliche", + "Text": "Text", + "Integer": "Ganze Zahl", + "Toggle": "Umschalten", + "Date": "Datum" }, "modals": { "Cancel": "Abbrechen", diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 0f30660a..b3366170 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -688,7 +688,11 @@ "Cannot drop items into Hidden Fields": "Cannot drop items into Hidden Fields", "Clear": "Clear", "Hidden Fields cannot be reordered": "Hidden Fields cannot be reordered", - "Select All": "Select All" + "Select All": "Select All", + "Visible {{label}}": "Visible {{label}}", + "Hide {{label}}": "Hide {{label}}", + "Hidden {{label}}": "Hidden {{label}}", + "Show {{label}}": "Show {{label}}" }, "WelcomeQuestions": { "Education": "Education", @@ -747,7 +751,19 @@ "menus": { "* Workspaces are available on team plans. ": "* Workspaces are available on team plans. ", "Select fields": "Select fields", - "Upgrade now": "Upgrade now" + "Upgrade now": "Upgrade now", + "Any": "Any", + "Numeric": "Numeric", + "Text": "Text", + "Integer": "Integer", + "Toggle": "Toggle", + "Date": "Date", + "DateTime": "DateTime", + "Choice": "Choice", + "Choice List": "Choice List", + "Reference": "Reference", + "Reference List": "Reference List", + "Attachment": "Attachment" }, "modals": { "Cancel": "Cancel", diff --git a/static/locales/es.client.json b/static/locales/es.client.json index fe112dea..b74568df 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -601,7 +601,11 @@ "Cannot drop items into Hidden Fields": "No se pueden colocar elementos en campos ocultos", "Clear": "Limpiar", "Hidden Fields cannot be reordered": "Los campos ocultos no pueden ser reordenados", - "Select All": "Seleccionar todo" + "Select All": "Seleccionar todo", + "Hide {{label}}": "Ocultar {{label}}", + "Hidden {{label}}": "{{label}} oculta", + "Show {{label}}": "Mostrar {{label}}", + "Visible {{label}}": "{{label}} visible" }, "WelcomeQuestions": { "Education": "Educación", @@ -808,7 +812,19 @@ "menus": { "* Workspaces are available on team plans. ": "* Los espacios de trabajo están disponibles en los planes de equipo. ", "Select fields": "Seleccionar campos", - "Upgrade now": "Actualizar ahora" + "Upgrade now": "Actualizar ahora", + "Numeric": "Numérico", + "Text": "Texto", + "Integer": "Entero", + "Date": "Fecha", + "DateTime": "Fecha y hora", + "Choice": "Elección", + "Choice List": "Lista de opciones", + "Reference": "Referencia", + "Reference List": "Lista de referencia", + "Attachment": "Adjunto", + "Any": "Cualquiera", + "Toggle": "Cambiar" }, "modals": { "Cancel": "Cancelar", diff --git a/static/locales/it.client.json b/static/locales/it.client.json index fdfcb46f..a67bdbe7 100644 --- a/static/locales/it.client.json +++ b/static/locales/it.client.json @@ -653,7 +653,8 @@ "Set formula": "Imposta formula", "Set trigger formula": "Imposta formula trigger", "TRIGGER FORMULA": "FORMULA TRIGGER", - "Make into data column": "Trasforma in una colonna di dati" + "Make into data column": "Trasforma in una colonna di dati", + "DESCRIPTION": "DESCRIZIONE" }, "FieldMenus": { "Save as common settings": "Salva come impostazioni comuni", @@ -832,7 +833,11 @@ "Cannot drop items into Hidden Fields": "Impossibile collocare elementi in campi nascosti", "Clear": "Svuota", "Hidden Fields cannot be reordered": "Impossibile riordinare i campi nascosti", - "Select All": "Seleziona tutto" + "Select All": "Seleziona tutto", + "Hidden {{label}}": "Nascosta {{label}}", + "Hide {{label}}": "Nascondi {{label}}", + "Visible {{label}}": "Visibile {{label}}", + "Show {{label}}": "Mostra {{label}}" }, "WidgetTitle": { "Cancel": "Annulla", @@ -853,7 +858,19 @@ "menus": { "* Workspaces are available on team plans. ": "*Gli spazi di lavoro sono disponibili nel piano per i team. ", "Select fields": "Seleziona campi", - "Upgrade now": "Aggiorna ora" + "Upgrade now": "Aggiorna ora", + "Any": "Qualsiasi", + "Text": "Testo", + "Integer": "Intero", + "Toggle": "Interruttore", + "Date": "Data", + "DateTime": "Data/ora", + "Reference": "Riferimento", + "Reference List": "Lista di riferimenti", + "Choice List": "Scelta da lista", + "Attachment": "Allegato", + "Numeric": "Numerico", + "Choice": "Scelta" }, "modals": { "Cancel": "Annulla", diff --git a/static/locales/pl.client.json b/static/locales/pl.client.json index e312790a..4c479bd7 100644 --- a/static/locales/pl.client.json +++ b/static/locales/pl.client.json @@ -344,7 +344,8 @@ "Empty Columns_other": "Puste kolumny", "Enter formula": "Wprowadź formułę", "Make into data column": "Zrób z tego kolumnę danych", - "Set trigger formula": "Ustawianie formuły wyzwalacza" + "Set trigger formula": "Ustawianie formuły wyzwalacza", + "DESCRIPTION": "OPIS" }, "GridOptions": { "Zebra Stripes": "Paski zebry", @@ -412,6 +413,53 @@ "All Documents": "Wszystkie dokumenty", "Create Empty Document": "Utwórz pusty dokument", "Create Workspace": "Utwórz przestrzeń roboczą", - "Access Details": "Szczegóły dostępu" + "Access Details": "Szczegóły dostępu", + "Rename": "Zmień nazwę", + "Trash": "Kosz", + "Workspaces": "Obszary robocze", + "Workspace will be moved to Trash.": "Obszar roboczy zostanie przeniesiony do kosza.", + "Import Document": "Importuj dokument", + "Manage Users": "Zarządzanie użytkownikami" + }, + "Importer": { + "Select fields to match on": "Wybierz pola do dopasowania", + "Merge rows that match these fields:": "Scal wiersze, które pasują do tych pól:", + "Update existing records": "Aktualizowanie istniejących rekordów" + }, + "MakeCopyMenu": { + "Update": "Aktualizacja", + "Update Original": "Zaktualizuj oryginał", + "Workspace": "Obszar roboczy", + "You do not have write access to the selected workspace": "Nie masz dostępu do zapisu w wybranej przestrzeni roboczej", + "Cancel": "Anuluj", + "As Template": "Jako szablon", + "Include the structure without any of the data.": "Dołącz strukturę bez żadnych danych.", + "Enter document name": "Wprowadź nazwę dokumentu", + "However, it appears to be already identical.": "Wydaje się jednak, że jest już identyczny.", + "It will be overwritten, losing any content not in this document.": "Zostanie on nadpisany, tracąc wszelkie treści nie znajdujące się w tym dokumencie.", + "Name": "Nazwa", + "No destination workspace": "Brak docelowej przestrzeni roboczej", + "Organization": "Organizacja", + "Overwrite": "Nadpisz", + "Original Has Modifications": "Oryginał ma modyfikacje", + "Original Looks Identical": "Oryginał Wygląda identycznie", + "To save your changes, please sign up, then reload this page.": "Aby zapisać zmiany, proszę się zarejestrować, a następnie ponownie załadować tę stronę.", + "Replacing the original requires editing rights on the original document.": "Zastąpienie oryginału wymaga uprawnień do edycji oryginalnego dokumentu.", + "Sign up": "Zarejestruj się", + "The original version of this document will be updated.": "Oryginalna wersja tego dokumentu zostanie zaktualizowana.", + "You do not have write access to this site": "Nie masz dostępu do zapisu na tej stronie", + "Original Looks Unrelated": "Oryginalny wygląd niepowiązany", + "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Uważaj, oryginał ma zmiany, których nie ma w tym dokumencie. Te zmiany zostaną nadpisane." + }, + "NotifyUI": { + "Go to your free personal site": "Przejdź do swojej darmowej strony osobistej", + "No notifications": "Brak powiadomień", + "Ask for help": "Zapytaj o pomoc", + "Cannot find personal site, sorry!": "Nie mogę znaleźć osobistej strony, przepraszam!", + "Give feedback": "Przekaż opinię", + "Notifications": "Powiadomienia" + }, + "LeftPanelCommon": { + "Help Center": "Centrum pomocy" } } diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index 066c32d7..2c4be4a1 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -744,7 +744,11 @@ "Cannot drop items into Hidden Fields": "Não é possível lançar itens em Campos Ocultos", "Clear": "Limpar", "Hidden Fields cannot be reordered": "Campos ocultos não podem ser reordenados", - "Select All": "Selecionar Todos" + "Select All": "Selecionar Todos", + "Hidden {{label}}": "{{label}} escondido", + "Show {{label}}": "Mostrar {{label}}", + "Visible {{label}}": "{{label}} visível", + "Hide {{label}}": "Ocultar {{label}}" }, "WelcomeQuestions": { "Education": "Educação", @@ -803,7 +807,19 @@ "menus": { "* Workspaces are available on team plans. ": "* As áreas de trabalho estão disponíveis nos planos de equipe. ", "Select fields": "Selecionar campos", - "Upgrade now": "Atualizar agora" + "Upgrade now": "Atualizar agora", + "Numeric": "Numérico", + "Text": "Texto", + "Integer": "Inteiro", + "Toggle": "Alternar", + "Date": "Data", + "DateTime": "DataHora", + "Choice List": "Lista de opções", + "Reference List": "Lista de Referências", + "Attachment": "Anexo", + "Any": "Qualquer", + "Choice": "Opção", + "Reference": "Referência" }, "modals": { "Cancel": "Cancelar", diff --git a/static/locales/ru.client.json b/static/locales/ru.client.json index e07ae771..71fd3ab8 100644 --- a/static/locales/ru.client.json +++ b/static/locales/ru.client.json @@ -4,7 +4,7 @@ "Attribute name": "Имя атрибута", "Add Default Rule": "Добавить правило по умолчанию", "Add Column Rule": "Добавить правило столбца", - "View As": "Посмотреть как", + "View As": "Смотреть как", "Seed rules": "Наследуемые правила", "Add User Attributes": "Добавить атрибуты пользователя", "Add Table Rules": "Добавить правила таблицы", @@ -798,7 +798,11 @@ "Clear": "Очистить", "Hidden Fields cannot be reordered": "Скрытые поля не могут быть переупорядочены", "Cannot drop items into Hidden Fields": "Невозможно поместить элементы в скрытые поля", - "Select All": "Выбрать все" + "Select All": "Выбрать все", + "Hide {{label}}": "Скрыть {{label}}", + "Hidden {{label}}": "Скрытый {{label}}", + "Show {{label}}": "Показать {{label}}", + "Visible {{label}}": "Видимый {{label}}" }, "WelcomeQuestions": { "Marketing": "Маркетинг", @@ -826,7 +830,19 @@ "menus": { "Select fields": "Выберите поля", "Upgrade now": "Обновитесь сейчас", - "* Workspaces are available on team plans. ": "* Рабочие пространства доступны в командных тарифах. " + "* Workspaces are available on team plans. ": "* Рабочие пространства доступны в командных тарифах. ", + "Any": "Любые", + "Numeric": "Численный", + "Text": "Текст", + "Toggle": "Переключатель", + "Date": "Дата", + "Choice": "Выбор", + "Reference List": "Ссылки списком", + "Choice List": "Выбор списком", + "Attachment": "Вложения", + "DateTime": "Дата и Время", + "Integer": "Целочисленный", + "Reference": "Ссылка" }, "modals": { "Cancel": "Отмена", diff --git a/test/mocha.opts b/test/mocha.opts index 7376bd86..cef2aeb3 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,5 @@ --require source-map-support/register test/report-why-tests-hang test/init-mocha-webdriver +test/split-tests test/chai-as-promised diff --git a/test/split-tests.js b/test/split-tests.js new file mode 100644 index 00000000..718875b6 --- /dev/null +++ b/test/split-tests.js @@ -0,0 +1,121 @@ +/** + * This module handles splitting tests for parallelizing them. This module is imported by any run + * of mocha, due by being listed in test/mocha.opts. + * + * It only does anything if TEST_SPLITS is set, which must have the form "3-of-8". + * + * If TEST_SPLITS is set to M-of-N, it is used to divide up all test suites in this mocha run into + * N groups, and runs the Mth of them. Note that M is 1-based, i.e. in [1..N] range. To have all + * tests run, each of the groups 1-of-N through N-of-N must run on the same total set of tests. + * + * The actual breaking into groups is informed by a timings file, defaulting to + * test/timings-all.txt. This has the format " ". + * Only those lines whose matches process.env.TEST_SUITE_FOR_TIMINGS will be used. + * + * The timings for test/timings-all.txt are prepared by our test reporter and written during + * Jenkins run as the timings/timings-all.txt artifact. After tests are added or changed, if + * timings may have changed significantly, it's good to update test/timings-all.txt, so that the + * parallel groups can be evened out as much as possible. + */ + +/* global before */ +const fs = require('fs'); +const { assert } = require('chai'); + +const testSuite = process.env.TEST_SUITE_FOR_TIMINGS || "unset_suite"; +const timingsFile = process.env.TIMINGS_FILE || "test/timings-all.txt"; + +before(function() { + const testSplits = process.env.TEST_SPLITS; + if (!testSplits) { + return; + } + const match = testSplits.match(/^(\d+)-of-(\d+)$/); + if (!match) { + assert.fail(`Invalid test split spec '${testSplits}': use format 'N-of-M'`); + } + + const group = Number(match[1]); + const groupCount = Number(match[2]); + if (!(group >= 1 && group <= groupCount)) { + assert.fail(`Invalid test split spec '${testSplits}': index must be in range 1..{groupCount}`); + } + + const testParent = this.test.parent; + const timings = getTimings(); + const groups = groupSuites(testParent.suites, timings, groupCount); + + testParent.suites = groups[group - 1]; // Convert to a 0-based index. + console.log(`Split tests groups; will run group ${group} of ${groupCount}`); +}); + +/** + * Read timings from timingsFile into a Map mapping file-suite-title to duration. + */ +function getTimings() { + const timings = new Map(); + try { + const content = fs.readFileSync(timingsFile, {encoding: 'utf8'}) + for (const line of content.split(/\r?\n/)) { + const [bigSuite, fileSuite, duration] = line.split(/\s+/); + if (bigSuite === testSuite && !isNaN(Number(duration))) { + timings.set(fileSuite, Number(duration)); + } + } + } catch (e) { + if (e.code === 'ENOENT') { + console.warn(`No timings found in ${timingsFile}; proceeding without timings`); + } else { + throw e; + } + } + return timings; +} + +/** + * Splits suites into groups and returns the list of them. + * + * The algorithm to group tests into suites starts goes one by one from longest to shortest, + * adding them to the least filled-up group. + */ +function groupSuites(suites, timings, groupCount) { + // Calculate a fallback value for durations as the average of existing durations. + const totalDuration = Array.from(timings.values()).reduce(((s, dur) => s + dur), 0); + if (!totalDuration) { + console.warn("No timings; assuming all tests are equally long"); + } + const fallbackDuration = totalDuration ? totalDuration / timings.size : 1000; + + const groups = Array.from(Array(groupCount), () => []); + const groupDurations = groups.map(() => 0); + + // Check for duplicate suite titles. + const suitesByTitle = new Map(suites.map(s => [s.title, s])); + for (const suite of suites) { + if (suitesByTitle.get(suite.title) !== suite) { + assert.fail(`Please fix duplicate suite title: ${suite.title}`); + } + } + + // Get timing for the given suite, falling back to fallbackDuration. + function getTiming(suite) { + const value = timings.get(suite.title); + return (typeof value !== 'number' || isNaN(value)) ? fallbackDuration : value; + } + + // Sort suites by descending duration. + const sortedSuites = suites.slice().sort((a, b) => getTiming(b) - getTiming(a)); + + for (const suite of sortedSuites) { + // Pick a least-duration group. + const index = groupDurations.indexOf(Math.min(...groupDurations)); + groups[index].push(suite); + groupDurations[index] += getTiming(suite); + } + + // Sort each group alphabetically by title. + for (const group of groups) { + group.sort((a, b) => a.title < b.title ? -1 : 1); + } + return groups; +} diff --git a/test/timings/nbrowser.txt b/test/timings/nbrowser.txt new file mode 100644 index 00000000..4c09ab9d --- /dev/null +++ b/test/timings/nbrowser.txt @@ -0,0 +1,28 @@ +nbrowser ActionLog 14737 +nbrowser ChoiceList 33037 +nbrowser CustomView 22055 +nbrowser CustomWidgets 14958 +nbrowser CustomWidgetsConfig 48287 +nbrowser DescriptionColumn 4649 +nbrowser DuplicateDocument 14042 +nbrowser Fork 112089 +nbrowser HomeIntro 44706 +nbrowser LanguageSettings 25427 +nbrowser Localization 10069 +nbrowser MultiColumn 455648 +nbrowser Pages 24986 +nbrowser ReferenceColumns 27590 +nbrowser ReferenceList 34333 +nbrowser RefTransforms 9072 +nbrowser RightPanel 10530 +nbrowser RightPanelSelectBy 6255 +nbrowser RowMenu 3702 +nbrowser saveViewSection 7596 +nbrowser SelectBy 5846 +nbrowser SelectByRefList 15186 +nbrowser SelectByRightPanel 3531 +nbrowser SelectBySummary 17516 +nbrowser SelectBySummaryRef 5382 +nbrowser SelectionSummary 6833 +nbrowser Smoke 1800 +nbrowser ToggleColumns 6530 diff --git a/test/timings/server.txt b/test/timings/server.txt new file mode 100644 index 00000000..589be516 --- /dev/null +++ b/test/timings/server.txt @@ -0,0 +1,7 @@ +server Comm 9557 +server generateInitialDocSql 1304 +server Authorizer 2375 +server DocApi 94358 +server DocApi2 730 +server HostedStorageManager 220307 +server backupSqliteDatabase 4348