mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
* Auto detect fixture usage in tests * Changes to runner for enabling e e2e tests * Only detect fixtures in test/UI/debug modes * Make scanning more resilient - require test.use, allow for multiple lines * Fix e2e/runner CI generate check * Make scanning context aware to match with running individual tests * Log enabled fixtures on one line * Rename gen-fixtures, move go gen to main.go * Return an error if no tests are matched * gen-ts-fixtures: Use text/template instead of manually writing to buf * Ignore generated fixtures.ts instead of formatting it * Fix pnpm lockfile patching for when e/e2e is missing * Simplify test running mode detection for scanning fixtures * go generate * Remove codegen from test run logic * Handle run.sh receiving e/e2e directly * Allow for fixtures to be enabled manually through flags * Fix logging of enabled fixtures to include the ones set by flags * Handle inline comments * Update cmd in generated file * Only pnpm install if teleport/connect needs building * Error if UI mode is invoked with connect tests * Handle errors better when checking if a binary exists * Add info to the README about manually enabling fixtures * Debug -> Warn * Add test files to --ui * Handle comments in strings properly * Remove connect TSH from pnpm install logic * Ignore flags in run.sh * Handle double quotes just in case * Add line number back * Handle commented out imports * Always include host.docker.internal in the certs * Only scan fixtures in test mode/when test files are specified * Make e2e/run.sh work with absolute paths to e/ tests * Scan fixtures when running tests but not in UI mode --------- Co-authored-by: Maja Cieślak <[email protected]>
372 lines
11 KiB
Go
372 lines
11 KiB
Go
/**
|
|
* Teleport
|
|
* Copyright (C) 2026 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gravitational/teleport/e2e/runner/fixtures"
|
|
)
|
|
|
|
func TestScanFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
wantNames []string
|
|
}{
|
|
{
|
|
name: "test.use with single fixture",
|
|
content: `test.use({ fixtures: ['ssh-node'] });`,
|
|
wantNames: []string{"ssh-node"},
|
|
},
|
|
{
|
|
name: "test.use with multiple fixtures",
|
|
content: `test.use({ fixtures: ['ssh-node', 'connect'] });`,
|
|
wantNames: []string{"ssh-node", "connect"},
|
|
},
|
|
{
|
|
name: "test.use with nested brackets",
|
|
content: `test.use({ fixtures: [['connect'], { option: true }] });`,
|
|
wantNames: []string{"connect"},
|
|
},
|
|
{
|
|
name: "bare fixtures array without test.use is ignored",
|
|
content: ` fixtures: [['connect'], { option: true }],`,
|
|
wantNames: nil,
|
|
},
|
|
{
|
|
name: "commented out line is skipped",
|
|
content: `// test.use({ fixtures: ['ssh-node'] });`,
|
|
wantNames: nil,
|
|
},
|
|
{
|
|
name: "no fixtures",
|
|
content: `test.use({ autoLogin: true });`,
|
|
wantNames: nil,
|
|
},
|
|
{
|
|
name: "mixed with other options",
|
|
content: `test.use({ autoLogin: true, fixtures: ['connect'] });`,
|
|
wantNames: []string{"connect"},
|
|
},
|
|
{
|
|
name: "multiline fixture array",
|
|
content: `test.use({
|
|
fixtures: [
|
|
'ssh-node',
|
|
'connect',
|
|
],
|
|
});`,
|
|
wantNames: []string{"ssh-node", "connect"},
|
|
},
|
|
{
|
|
name: "multiline with comments between",
|
|
content: `test.use({
|
|
fixtures: [
|
|
// 'ssh-node',
|
|
'connect',
|
|
],
|
|
});`,
|
|
wantNames: []string{"connect"},
|
|
},
|
|
{
|
|
name: "nested parens in options before fixtures",
|
|
content: `test.use({ timeout: getTimeout(), fixtures: ['ssh-node'] });`,
|
|
wantNames: []string{"ssh-node"},
|
|
},
|
|
{
|
|
name: "block comment is stripped",
|
|
content: `/* test.use({ fixtures: ['ssh-node'] }); */
|
|
test.use({ fixtures: ['connect'] });`,
|
|
wantNames: []string{"connect"},
|
|
},
|
|
{
|
|
name: "trailing inline comment is stripped",
|
|
content: `someCode; // test.use({ fixtures: ['connect'] })`,
|
|
wantNames: nil,
|
|
},
|
|
{
|
|
name: "inline comment after real fixture is stripped",
|
|
content: `test.use({ fixtures: ['ssh-node'] }); // test.use({ fixtures: ['connect'] })`,
|
|
wantNames: []string{"ssh-node"},
|
|
},
|
|
{
|
|
name: "braces inside string literals do not corrupt blocks",
|
|
content: "const s = \"{ not a block }\";\ntest.use({ fixtures: ['ssh-node'] });",
|
|
wantNames: []string{"ssh-node"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tmpFile := filepath.Join(dir, "test.spec.ts")
|
|
writeFile(t, dir, "test.spec.ts", tt.content)
|
|
|
|
got := scanFile(tmpFile, 0)
|
|
|
|
if len(got) != len(tt.wantNames) {
|
|
t.Fatalf("got %d fixtures, want %d", len(got), len(tt.wantNames))
|
|
}
|
|
|
|
for i, f := range got {
|
|
if f.Name != tt.wantNames[i] {
|
|
t.Errorf("fixture[%d] name = %q, want %q", i, f.Name, tt.wantNames[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanFileLineScope(t *testing.T) {
|
|
content := `test.use({ fixtures: ['ssh-node'] }); // 1 (top-level)
|
|
// 2
|
|
test.describe('connect tests', () => { // 3
|
|
test.use({ fixtures: ['connect'] }); // 4
|
|
// 5
|
|
test('opens connect', async () => { // 6
|
|
// test body // 7
|
|
}); // 8
|
|
}); // 9
|
|
// 10
|
|
test.describe('web tests', () => { // 11
|
|
test('opens web', async () => { // 12
|
|
// test body // 13
|
|
}); // 14
|
|
}); // 15
|
|
// 16
|
|
test.describe(() => { // 17
|
|
test.use({ fixtures: ['connect'] }); // 18
|
|
test('one', async () => { // 19
|
|
// test body // 20
|
|
}); // 21
|
|
test('two', async () => { // 22
|
|
// test body // 23
|
|
}); // 24
|
|
}); // 25`
|
|
|
|
tests := []struct {
|
|
name string
|
|
targetLine int
|
|
wantNames []string
|
|
}{
|
|
{
|
|
name: "no line filter returns all fixtures",
|
|
targetLine: 0,
|
|
wantNames: []string{"ssh-node", "connect", "connect"},
|
|
},
|
|
{
|
|
name: "line inside connect describe gets top-level and connect",
|
|
targetLine: 7,
|
|
wantNames: []string{"ssh-node", "connect"},
|
|
},
|
|
{
|
|
name: "line inside web describe gets only top-level",
|
|
targetLine: 13,
|
|
wantNames: []string{"ssh-node"},
|
|
},
|
|
{
|
|
name: "line targeting specific test inside describe with test.use",
|
|
targetLine: 23,
|
|
wantNames: []string{"ssh-node", "connect"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tmpFile := filepath.Join(dir, "test.spec.ts")
|
|
writeFile(t, dir, "test.spec.ts", content)
|
|
|
|
got := scanFile(tmpFile, tt.targetLine)
|
|
|
|
if len(got) != len(tt.wantNames) {
|
|
t.Fatalf("got %d fixtures, want %d: %v", len(got), len(tt.wantNames), fixtureNames(got))
|
|
}
|
|
|
|
for i, f := range got {
|
|
if f.Name != tt.wantNames[i] {
|
|
t.Errorf("fixture[%d] name = %q, want %q", i, f.Name, tt.wantNames[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseHelperImports(t *testing.T) {
|
|
content := `import { test, expect } from '@gravitational/e2e/helpers/connect';
|
|
import { startUrl } from '@gravitational/e2e/helpers/env';
|
|
import { chromium } from '@playwright/test';
|
|
`
|
|
tmpFile := filepath.Join(t.TempDir(), "test.spec.ts")
|
|
if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := parseHelperImports(tmpFile)
|
|
want := []string{"connect", "env"}
|
|
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d imports, want %d: %v", len(got), len(want), got)
|
|
}
|
|
|
|
for i, h := range got {
|
|
if h != want[i] {
|
|
t.Errorf("import[%d] = %q, want %q", i, h, want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFixtures(t *testing.T) {
|
|
e2eDir := t.TempDir()
|
|
|
|
helpersDir := createDir(t, e2eDir, "helpers")
|
|
testsDir := createDir(t, e2eDir, "tests", "connect")
|
|
webDir := createDir(t, e2eDir, "tests", "web", "authenticated")
|
|
|
|
// Helper that declares Connect fixture as default.
|
|
connectHelper := `import { test as fixtureBase } from './fixtures';
|
|
export const test = fixtureBase.extend<{}>({});
|
|
test.use({ fixtures: ['connect'] });
|
|
`
|
|
writeFile(t, helpersDir, "connect.ts", connectHelper)
|
|
|
|
// Helper with no fixtures.
|
|
testHelper := `import { test as base } from './fixtures';
|
|
export const test = base.extend<{}>({});
|
|
`
|
|
writeFile(t, helpersDir, "test.ts", testHelper)
|
|
|
|
// Connect spec file imports from connect helper.
|
|
connectSpec := `import { test } from '@gravitational/e2e/helpers/connect';
|
|
test('something', async () => {});
|
|
`
|
|
writeFile(t, testsDir, "auth.spec.ts", connectSpec)
|
|
|
|
// Web spec file imports from test helper (no fixtures).
|
|
webSpec := `import { test } from '@gravitational/e2e/helpers/test';
|
|
test('something', async () => {});
|
|
`
|
|
writeFile(t, webDir, "roles.spec.ts", webSpec)
|
|
|
|
t.Run("connect test detects Connect fixture via helper", func(t *testing.T) {
|
|
rel, _ := filepath.Rel(e2eDir, filepath.Join(testsDir, "auth.spec.ts"))
|
|
got := scanFixtures(e2eDir, []string{rel})
|
|
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 fixture, got %d", len(got))
|
|
}
|
|
|
|
if got[0].Name != "connect" {
|
|
t.Errorf("expected fixture name 'connect', got %q", got[0].Name)
|
|
}
|
|
})
|
|
|
|
t.Run("web test does not detect Connect fixture", func(t *testing.T) {
|
|
rel, _ := filepath.Rel(e2eDir, filepath.Join(webDir, "roles.spec.ts"))
|
|
got := scanFixtures(e2eDir, []string{rel})
|
|
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected 0 fixtures, got %d", len(got))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolveFilesToScan(t *testing.T) {
|
|
e2eDir := t.TempDir()
|
|
testsDir := createDir(t, e2eDir, "tests", "connect")
|
|
|
|
writeFile(t, testsDir, "auth.spec.ts", "test('auth', async () => {});")
|
|
writeFile(t, testsDir, "session.spec.ts", "test('session', async () => {});")
|
|
|
|
t.Run("file with line number", func(t *testing.T) {
|
|
rel, _ := filepath.Rel(e2eDir, filepath.Join(testsDir, "auth.spec.ts"))
|
|
targets, err := resolveFilesToScan(e2eDir, []string{rel + ":42"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(targets) != 1 {
|
|
t.Fatalf("expected 1 target, got %d", len(targets))
|
|
}
|
|
|
|
if targets[0].line != 42 {
|
|
t.Errorf("expected line 42, got %d", targets[0].line)
|
|
}
|
|
})
|
|
|
|
t.Run("directory expands to spec files", func(t *testing.T) {
|
|
targets, err := resolveFilesToScan(e2eDir, []string{"tests/connect"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(targets) != 2 {
|
|
t.Fatalf("expected 2 targets, got %d", len(targets))
|
|
}
|
|
|
|
for _, tgt := range targets {
|
|
if tgt.line != 0 {
|
|
t.Errorf("directory target should have line=0, got %d", tgt.line)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("substring filter matches spec files", func(t *testing.T) {
|
|
targets, err := resolveFilesToScan(e2eDir, []string{"auth"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(targets) != 1 {
|
|
t.Fatalf("expected 1 target, got %d", len(targets))
|
|
}
|
|
})
|
|
}
|
|
|
|
func createDir(t *testing.T, path ...string) string {
|
|
t.Helper()
|
|
|
|
dir := filepath.Join(path...)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatalf("creating directory %s: %v", dir, err)
|
|
}
|
|
|
|
return dir
|
|
}
|
|
|
|
func writeFile(t *testing.T, dir, name, content string) {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatalf("writing file %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func fixtureNames(ff []*fixtures.Fixture) []string {
|
|
names := make([]string, len(ff))
|
|
for i, f := range ff {
|
|
names[i] = f.Name
|
|
}
|
|
return names
|
|
}
|