diff --git a/.gitea/workflows/test-pr.yml b/.gitea/workflows/test-pr.yml index aecff5d..a9573c9 100644 --- a/.gitea/workflows/test-pr.yml +++ b/.gitea/workflows/test-pr.yml @@ -12,13 +12,9 @@ jobs: # uses: golang/govulncheck-action@v1 # with: # go-version-file: 'go.mod' - check-and-test: + check-and-unit: + name: Lint Build And Unit Coverage runs-on: ubuntu-latest - env: - HTTP_PROXY: "" - GITEA_TEA_TEST_URL: "http://gitea:3000" - GITEA_TEA_TEST_USERNAME: "test01" - GITEA_TEA_TEST_PASSWORD: "test01" steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -32,11 +28,27 @@ jobs: make fmt-check make docs-check make build - - run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance - - name: test and coverage + - name: unit test and coverage run: | - make test make unit-test-coverage + + integration-test: + name: Integration Test + runs-on: ubuntu-latest + env: + HTTP_PROXY: "" + GITEA_TEA_TEST_URL: "http://gitea:3000" + GITEA_TEA_TEST_USERNAME: "test01" + GITEA_TEA_TEST_PASSWORD: "test01" + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + - run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance + - name: integration test + run: | + make integration-test services: gitea: image: docker.gitea.com/gitea:1.26.1 diff --git a/Makefile b/Makefile index 487461c..31ff016 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,10 @@ LDFLAGS := -X "code.gitea.io/tea/modules/version.Version=$(TEA_VERSION)" -X "cod # override to allow passing additional goflags via make CLI override GOFLAGS := $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -PACKAGES ?= $(shell $(GO) list ./...) +PACKAGES ?= $(shell $(GO) list ./... | grep -v '^code.gitea.io/tea/tests') +UNIT_PACKAGES ?= $(PACKAGES) +INTEGRATION_PACKAGES ?= $(shell $(GO) list ./tests/... 2>/dev/null) +INTEGRATION_TEST_TAGS ?= testtools SOURCES ?= $(shell find . -name "*.go" -type f) # OS specific vars. @@ -64,11 +67,11 @@ vet: .PHONY: lint lint: - $(GO) run $(GOLANGCI_LINT_PACKAGE) run + $(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools .PHONY: lint-fix lint-fix: - $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix + $(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools --fix .PHONY: fmt-check fmt-check: @@ -93,13 +96,24 @@ docs-check: exit 1; \ fi; +.PHONY: unit-test +unit-test: + $(GO) test $(UNIT_PACKAGES) + +.PHONY: integration-test +integration-test: + @if [ -n "$(INTEGRATION_PACKAGES)" ]; then \ + $(GO) test -tags='$(INTEGRATION_TEST_TAGS)' $(INTEGRATION_PACKAGES); \ + else \ + echo "No integration test packages found"; \ + fi + .PHONY: test -test: - $(GO) test -tags='sqlite sqlite_unlock_notify' $(PACKAGES) +test: unit-test integration-test .PHONY: unit-test-coverage unit-test-coverage: - $(GO) test -tags='sqlite sqlite_unlock_notify' -cover -coverprofile coverage.out $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 + $(GO) test -cover -coverprofile coverage.out $(UNIT_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 .PHONY: tidy tidy: @@ -122,4 +136,3 @@ $(EXECUTABLE): $(SOURCES) .PHONY: build-image build-image: docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) . - diff --git a/modules/config/testtools.go b/modules/config/testtools.go new file mode 100644 index 0000000..384cfe2 --- /dev/null +++ b/modules/config/testtools.go @@ -0,0 +1,13 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build testtools + +package config + +import "time" + +// AcquireConfigLockForTesting exposes the internal lock helper to integration tests. +func AcquireConfigLockForTesting(lockPath string, timeout time.Duration) (func() error, error) { + return acquireConfigLock(lockPath, timeout) +} diff --git a/modules/context/context_test.go b/modules/context/context_test.go index 2c8ea43..9b73f88 100644 --- a/modules/context/context_test.go +++ b/modules/context/context_test.go @@ -4,14 +4,9 @@ package context import ( - "os" - "os/exec" "testing" "code.gitea.io/tea/modules/config" - - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v3" ) func Test_MatchLogins(t *testing.T) { @@ -70,47 +65,3 @@ func Test_MatchLogins(t *testing.T) { }) } } - -func TestInitCommand_WithRepoSlugSkipsLocalRepoDetection(t *testing.T) { - tmpDir := t.TempDir() - config.SetConfigForTesting(config.LocalConfig{ - Logins: []config.Login{{ - Name: "test-login", - URL: "https://gitea.example.com", - Token: "token", - User: "login-user", - Default: true, - }}, - }) - - cmd := exec.Command("git", "init", "--object-format=sha256", tmpDir) - cmd.Env = os.Environ() - require.NoError(t, cmd.Run()) - - oldWd, err := os.Getwd() - require.NoError(t, err) - require.NoError(t, os.Chdir(tmpDir)) - t.Cleanup(func() { - require.NoError(t, os.Chdir(oldWd)) - }) - - cliCmd := cli.Command{ - Name: "branches", - Flags: []cli.Flag{ - &cli.StringFlag{Name: "login"}, - &cli.StringFlag{Name: "repo"}, - &cli.StringFlag{Name: "remote"}, - &cli.StringFlag{Name: "output"}, - }, - } - require.NoError(t, cliCmd.Set("repo", "owner/repo")) - - ctx, err := InitCommand(&cliCmd) - require.NoError(t, err) - require.Equal(t, "owner", ctx.Owner) - require.Equal(t, "repo", ctx.Repo) - require.Equal(t, "owner/repo", ctx.RepoSlug) - require.Nil(t, ctx.LocalRepo) - require.NotNil(t, ctx.Login) - require.Equal(t, "test-login", ctx.Login.Name) -} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..0d03110 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,10 @@ +This directory contains integration tests that exercise tea against external services or external executables. + +- Unit tests stay next to the packages they cover. +- Integration tests live under `tests/` so they can be run separately. + +Common targets: + +- `make unit-test` +- `make integration-test` +- `make test` diff --git a/modules/config/lock_unix_test.go b/tests/integration/config_lock_unix_test.go similarity index 75% rename from modules/config/lock_unix_test.go rename to tests/integration/config_lock_unix_test.go index f8ab2f8..ff712c6 100644 --- a/modules/config/lock_unix_test.go +++ b/tests/integration/config_lock_unix_test.go @@ -3,7 +3,7 @@ //go:build unix -package config +package integration import ( "fmt" @@ -12,10 +12,11 @@ import ( "path/filepath" "testing" "time" + + "code.gitea.io/tea/modules/config" ) func TestConfigLock_CrossProcess(t *testing.T) { - // Create a temp directory for test tmpDir, err := os.MkdirTemp("", "tea-lock-test") if err != nil { t.Fatalf("failed to create temp dir: %v", err) @@ -24,15 +25,16 @@ func TestConfigLock_CrossProcess(t *testing.T) { lockPath := filepath.Join(tmpDir, "config.yml.lock") - // Acquire lock in main process - unlock, err := acquireConfigLock(lockPath, 5*time.Second) + unlock, err := config.AcquireConfigLockForTesting(lockPath, 5*time.Second) if err != nil { t.Fatalf("failed to acquire lock: %v", err) } - defer unlock() + defer func() { + if err := unlock(); err != nil { + t.Fatalf("failed to release lock: %v", err) + } + }() - // Spawn a subprocess that tries to acquire the same lock - // The subprocess should fail to acquire within timeout script := fmt.Sprintf(` package main @@ -48,19 +50,16 @@ func main() { } defer file.Close() - // Try non-blocking lock err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) if err != nil { - // Lock is held - expected behavior os.Exit(0) } - // Lock was acquired - unexpected + syscall.Flock(int(file.Fd()), syscall.LOCK_UN) os.Exit(1) } `, lockPath) - // Write and run the test script scriptPath := filepath.Join(tmpDir, "locktest.go") if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil { t.Fatalf("failed to write test script: %v", err) @@ -78,5 +77,4 @@ func main() { t.Errorf("subprocess execution failed: %v", err) } } - // Exit code 0 means lock was properly held - success } diff --git a/tests/integration/context_init_test.go b/tests/integration/context_init_test.go new file mode 100644 index 0000000..a220af2 --- /dev/null +++ b/tests/integration/context_init_test.go @@ -0,0 +1,59 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "os" + "os/exec" + "testing" + + "code.gitea.io/tea/modules/config" + teacontext "code.gitea.io/tea/modules/context" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func TestInitCommand_WithRepoSlugSkipsLocalRepoDetection(t *testing.T) { + tmpDir := t.TempDir() + config.SetConfigForTesting(config.LocalConfig{ + Logins: []config.Login{{ + Name: "test-login", + URL: "https://gitea.example.com", + Token: "token", + User: "login-user", + Default: true, + }}, + }) + + cmd := exec.Command("git", "init", "--object-format=sha256", tmpDir) + cmd.Env = os.Environ() + require.NoError(t, cmd.Run()) + + oldWd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(tmpDir)) + t.Cleanup(func() { + require.NoError(t, os.Chdir(oldWd)) + }) + + cliCmd := cli.Command{ + Name: "branches", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "login"}, + &cli.StringFlag{Name: "repo"}, + &cli.StringFlag{Name: "remote"}, + &cli.StringFlag{Name: "output"}, + }, + } + require.NoError(t, cliCmd.Set("repo", "owner/repo")) + + ctx, err := teacontext.InitCommand(&cliCmd) + require.NoError(t, err) + require.Equal(t, "owner", ctx.Owner) + require.Equal(t, "repo", ctx.Repo) + require.Equal(t, "owner/repo", ctx.RepoSlug) + require.Nil(t, ctx.LocalRepo) + require.NotNil(t, ctx.Login) + require.Equal(t, "test-login", ctx.Login.Name) +} diff --git a/modules/git/repo_test.go b/tests/integration/git_repo_test.go similarity index 80% rename from modules/git/repo_test.go rename to tests/integration/git_repo_test.go index b9b339a..5dbf9f4 100644 --- a/modules/git/repo_test.go +++ b/tests/integration/git_repo_test.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package integration import ( "os" @@ -9,11 +9,11 @@ import ( "path/filepath" "testing" + teagit "code.gitea.io/tea/modules/git" "github.com/stretchr/testify/assert" ) func TestRepoFromPath_Worktree(t *testing.T) { - // Create a temporary directory for test tmpDir, err := os.MkdirTemp("", "tea-worktree-test-*") assert.NoError(t, err) defer os.RemoveAll(tmpDir) @@ -21,21 +21,17 @@ func TestRepoFromPath_Worktree(t *testing.T) { mainRepoPath := filepath.Join(tmpDir, "main-repo") worktreePath := filepath.Join(tmpDir, "worktree") - // Initialize main repository cmd := exec.Command("git", "init", mainRepoPath) assert.NoError(t, cmd.Run()) - // Configure git for the test cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.email", "test@example.com") assert.NoError(t, cmd.Run()) cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User") assert.NoError(t, cmd.Run()) - // Add a remote to the main repository cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git") assert.NoError(t, cmd.Run()) - // Create an initial commit (required for worktree) readmePath := filepath.Join(mainRepoPath, "README.md") err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644) assert.NoError(t, err) @@ -44,19 +40,14 @@ func TestRepoFromPath_Worktree(t *testing.T) { cmd = exec.Command("git", "-C", mainRepoPath, "commit", "-m", "Initial commit") assert.NoError(t, cmd.Run()) - // Create a worktree cmd = exec.Command("git", "-C", mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch") assert.NoError(t, cmd.Run()) - // Test: Open repository from worktree path - repo, err := RepoFromPath(worktreePath) + repo, err := teagit.RepoFromPath(worktreePath) assert.NoError(t, err, "Should be able to open worktree") - // Test: Read config from worktree (should read from main repo's config) config, err := repo.Config() assert.NoError(t, err, "Should be able to read config") - - // Verify that remotes are accessible from worktree assert.NotEmpty(t, config.Remotes, "Should be able to read remotes from worktree") assert.Contains(t, config.Remotes, "origin", "Should have origin remote") assert.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0], "Should have correct remote URL") diff --git a/cmd/repos/create_test.go b/tests/integration/repos_create_test.go similarity index 56% rename from cmd/repos/create_test.go rename to tests/integration/repos_create_test.go index acb42aa..07d79d7 100644 --- a/cmd/repos/create_test.go +++ b/tests/integration/repos_create_test.go @@ -1,28 +1,66 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repos +package integration import ( "context" "fmt" "os" + "path/filepath" "testing" "time" "code.gitea.io/sdk/gitea" + "code.gitea.io/tea/cmd/repos" + "code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/task" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/urfave/cli/v3" ) +func useTempConfigPath(t *testing.T) string { + t.Helper() + + configPath := filepath.Join(t.TempDir(), "config.yml") + config.SetConfigPathForTesting(configPath) + t.Cleanup(func() { + config.SetConfigPathForTesting("") + }) + + return configPath +} + +func createIntegrationLogin(t *testing.T, giteaURL string) *config.Login { + t.Helper() + + _ = useTempConfigPath(t) + + username := os.Getenv("GITEA_TEA_TEST_USERNAME") + password := os.Getenv("GITEA_TEA_TEST_PASSWORD") + require.NotEmpty(t, username, "GITEA_TEA_TEST_USERNAME is required for integration tests") + require.NotEmpty(t, password, "GITEA_TEA_TEST_PASSWORD is required for integration tests") + + require.NoError(t, task.CreateLogin("integration", "", username, password, "", "", "", giteaURL, "", "", true, false, false, false)) + + login, err := config.GetLoginByName("integration") + require.NoError(t, err) + require.NotNil(t, login) + + return login +} + func TestCreateRepoObjectFormat(t *testing.T) { giteaURL := os.Getenv("GITEA_TEA_TEST_URL") if giteaURL == "" { t.Skip("GITEA_TEA_TEST_URL is not set, skipping test") } + login := createIntegrationLogin(t, giteaURL) + client := login.Client() timestamp := time.Now().Unix() + tests := []struct { name string args []string @@ -56,22 +94,15 @@ func TestCreateRepoObjectFormat(t *testing.T) { }, } - giteaUserName := os.Getenv("GITEA_TEA_TEST_USERNAME") - giteaUserPasword := os.Getenv("GITEA_TEA_TEST_PASSWORD") - - err := task.CreateLogin("test", "", giteaUserName, giteaUserPasword, "", "", "", giteaURL, "", "", true, false, false, false) - if err != nil && err.Error() != "login name 'test' has already been used" { - t.Fatal(err) - } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reposCmd := &cli.Command{ Name: "repos", - Commands: []*cli.Command{&CmdRepoCreate}, + Commands: []*cli.Command{&repos.CmdRepoCreate}, } - tt.args = append(tt.args, "--login", "test") + args := append([]string{"repos", "create"}, tt.args...) + args = append(args, "--login", login.Name) err := reposCmd.Run(context.Background(), args) if tt.wantErr { @@ -82,7 +113,12 @@ func TestCreateRepoObjectFormat(t *testing.T) { return } - assert.NoError(t, err) + require.NoError(t, err) + t.Cleanup(func() { + if _, delErr := client.DeleteRepo(login.User, tt.wantOpts.Name); delErr != nil { + t.Logf("failed to delete integration test repo %q: %v", tt.wantOpts.Name, delErr) + } + }) }) } }