Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#
# Copyright (c) 2020-2024 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
name: Unit Tests & Coverage

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
unit-tests:
name: Run unit tests
runs-on: ubuntu-24.04
permissions:
id-token: write

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Run unit tests with coverage
run: make test-coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
use_oidc: true
flags: unit-tests
files: ./coverage.out
fail_ci_if_error: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
_output
generated
coverage.out
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ include build/makefiles/version.mk

all: help

.PHONY: test test-coverage

### test: Run unit tests
test:
go test -v ./pkg/...

### test-coverage: Run unit tests with coverage report
test-coverage:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test-coverage:
.PHONY: test test-coverage
test-coverage:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added .PHONY declaration. Thanks.

go test -v -coverprofile=coverage.out -covermode=atomic ./pkg/...
go tool cover -func=coverage.out

_print_vars:
echo "Current env vars:"
echo " WTO_IMG=$(WTO_IMG)"
Expand Down
34 changes: 34 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
codecov:
require_ci_to_pass: yes

coverage:
precision: 2
round: down
range: "50...100"

status:
project:
default:
target: auto
threshold: 1%
patch:
default:
target: auto
threshold: 1%

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking what behavior we're expecting from the Codecov configuration.

Is target: auto with a 1% threshold intentional here? Does this configuration actually enforce a minimum coverage level, or is the goal only to track/report coverage?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intentional and follows the standard configuration from the Code Coverage Onboarding Guide. target: auto sets the target to the base commit's coverage (not a fixed minimum), and threshold: 1% means the Codecov status check passes as long as coverage doesn't drop more than 1% from the base. So it's reporting + soft enforcement. It flags regressions but doesn't impose a fixed floor.


ignore:
- "vendor/**"
- "build/**"
- "hack/**"
- "docs/**"
- "manifests/**"
- "metadata/**"
- "dev-scripts/**"

flags:
unit-tests:
carryforward: true

comment:
layout: "reach,diff,flags,files"
behavior: default
91 changes: 91 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright (c) 2021-2024 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//

package config

import (
"testing"
)

func TestGetDefaultToolingImage(t *testing.T) {
tests := []struct {
name string
envVal string
want string
wantErr bool
}{
{
name: "returns image when env var is set",
envVal: "quay.io/wto/web-terminal-tooling:latest",
want: "quay.io/wto/web-terminal-tooling:latest",
wantErr: false,
},
{
name: "returns error when env var is unset",
envVal: "",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(toolingImageEnvVar, tt.envVal)
got, err := GetDefaultToolingImage()
if (err != nil) != tt.wantErr {
t.Errorf("GetDefaultToolingImage() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetDefaultToolingImage() = %v, want %v", got, tt.want)
}
})
}
}

func TestGetDefaultExecImage(t *testing.T) {
tests := []struct {
name string
envVal string
want string
wantErr bool
}{
{
name: "returns image when env var is set",
envVal: "quay.io/wto/web-terminal-exec:latest",
want: "quay.io/wto/web-terminal-exec:latest",
wantErr: false,
},
{
name: "returns error when env var is unset",
envVal: "",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(execImageEnvVar, tt.envVal)
got, err := GetDefaultExecImage()
if (err != nil) != tt.wantErr {
t.Errorf("GetDefaultExecImage() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetDefaultExecImage() = %v, want %v", got, tt.want)
}
})
}
}

func TestGetNamespaceReturnsErrorOutsideCluster(t *testing.T) {
_, err := GetNamespace()
if err == nil {
t.Fatal("expected error when service account namespace file does not exist")
}
}
157 changes: 157 additions & 0 deletions pkg/webterminal/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// Copyright (c) 2021-2024 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//

package webterminal

import (
"testing"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/redhat-developer/web-terminal-operator/pkg/config"
)

func makeExecTemplate(image string) *dw.DevWorkspaceTemplate {
return &dw.DevWorkspaceTemplate{
ObjectMeta: v1.ObjectMeta{
Name: config.ExecTemplateName,
Namespace: "test-ns",
},
Spec: dw.DevWorkspaceTemplateSpec{
DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{
Components: []dw.Component{
{
Name: config.ExecTemplateName,
ComponentUnion: dw.ComponentUnion{
Container: &dw.ContainerComponent{
Container: dw.Container{
Image: image,
},
},
},
},
},
},
},
}
}

func TestHandleUnmanagedExecState(t *testing.T) {
const defaultImage = "quay.io/wto/web-terminal-exec:latest"

tests := []struct {
name string
specImage string
clusterImage string
expectedImage string
}{
{
name: "same image returns cluster unchanged",
specImage: defaultImage,
clusterImage: defaultImage,
expectedImage: defaultImage,
},
{
name: "same repo different tag updates image",
specImage: defaultImage,
clusterImage: "quay.io/wto/web-terminal-exec:old",
expectedImage: defaultImage,
},
{
name: "same repo different digest updates image",
specImage: defaultImage,
clusterImage: "quay.io/wto/web-terminal-exec@sha256:abc123",
expectedImage: defaultImage,
},
{
name: "different repo does not update",
specImage: defaultImage,
clusterImage: "quay.io/other/custom-exec:latest",
expectedImage: "quay.io/other/custom-exec:latest",
},
{
name: "empty cluster image returns cluster unchanged",
specImage: defaultImage,
clusterImage: "",
expectedImage: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("RELATED_IMAGE_web_terminal_exec", tt.specImage)

spec := makeExecTemplate(tt.specImage)
cluster := makeExecTemplate(tt.clusterImage)

result := handleUnmanagedExecState(spec, cluster)

var resultImage string
for _, component := range result.Spec.Components {
if component.Name == config.ExecTemplateName && component.Container != nil {
resultImage = component.Container.Image
}
}
if resultImage != tt.expectedImage {
t.Errorf("expected image %q, got %q", tt.expectedImage, resultImage)
}
})
}
}

func TestHandleUnmanagedExecStateEnvUnset(t *testing.T) {
t.Setenv("RELATED_IMAGE_web_terminal_exec", "")

spec := makeExecTemplate("any-image")
cluster := makeExecTemplate("quay.io/wto/web-terminal-exec:old")

result := handleUnmanagedExecState(spec, cluster)

var resultImage string
for _, component := range result.Spec.Components {
if component.Name == config.ExecTemplateName && component.Container != nil {
resultImage = component.Container.Image
}
}
if resultImage != "quay.io/wto/web-terminal-exec:old" {
t.Errorf("expected cluster image unchanged when env var unset, got %q", resultImage)
}
}

func TestGetSpecExecTemplate(t *testing.T) {
const testImage = "quay.io/wto/web-terminal-exec:test"
t.Setenv("RELATED_IMAGE_web_terminal_exec", testImage)

dwt, err := getSpecExecTemplate("test-namespace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dwt.Name != config.ExecTemplateName {
t.Errorf("expected name %q, got %q", config.ExecTemplateName, dwt.Name)
}
if dwt.Namespace != "test-namespace" {
t.Errorf("expected namespace %q, got %q", "test-namespace", dwt.Namespace)
}
if len(dwt.Spec.Components) != 1 {
t.Fatalf("expected 1 component, got %d", len(dwt.Spec.Components))
}
if dwt.Spec.Components[0].Container.Image != testImage {
t.Errorf("expected image %q, got %q", testImage, dwt.Spec.Components[0].Container.Image)
}
}

func TestGetSpecExecTemplateEnvUnset(t *testing.T) {
t.Setenv("RELATED_IMAGE_web_terminal_exec", "")

_, err := getSpecExecTemplate("test-namespace")
if err == nil {
t.Error("expected error when env var is unset")
}
}
Loading
Loading