Skip to content

Conversation

@lkingland
Copy link
Member

@lkingland lkingland commented Jul 15, 2025

Unifies E2E tests and workflows

/kind cleanup

Primary Changes:

  • Merges all E2E scenarios (test/*) into a single e2e/e2e_test.go which is executable locally during development.
  • Workflows mostly consolidated into test-and-publish.yaml.
  • make test-full target runs all tests (unit, integration, E2E).
  • Extracts logic from workflows into scripts or make targets
  • etc. etc. See the e2e/README.md

Notable Name Changes: the scripts in ./hack were renamed to be simple nouns:
hack/cluster.sh
hack/binaries.sh
hack/images.sh
hack/gitlab.sh

@knative-prow
Copy link

knative-prow bot commented Jul 15, 2025

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@knative-prow knative-prow bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. labels Jul 15, 2025
@knative-prow knative-prow bot requested review from dsimansk and jrangelramos July 15, 2025 11:26
@knative-prow
Copy link

knative-prow bot commented Jul 15, 2025

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: lkingland

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@knative-prow knative-prow bot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Jul 15, 2025
@lkingland lkingland force-pushed the push-tzvwvvkmvwtq branch from 94c8089 to 7512faf Compare July 15, 2025 11:30
@codecov
Copy link

codecov bot commented Jul 15, 2025

Codecov Report

❌ Patch coverage is 42.85714% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.75%. Comparing base (0a86ac4) to head (c069c6f).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pkg/pipelines/tekton/validate.go 33.33% 2 Missing ⚠️
cmd/completion_util.go 0.00% 1 Missing ⚠️
cmd/describe.go 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2938      +/-   ##
==========================================
- Coverage   59.37%   54.75%   -4.63%     
==========================================
  Files         134      134              
  Lines       13500    13494       -6     
==========================================
- Hits         8016     7389     -627     
- Misses       4540     5198     +658     
+ Partials      944      907      -37     
Flag Coverage Δ
e2e-tests ?
integration 17.41% <0.00%> (?)
integration-tests ?
unit macos-latest 45.43% <42.85%> (?)
unit ubuntu-latest 46.45% <42.85%> (?)
unit windows-latest 45.45% <42.85%> (?)
unit-tests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lkingland lkingland force-pushed the push-tzvwvvkmvwtq branch 4 times, most recently from b68e41f to e398349 Compare July 15, 2025 11:47
@lkingland lkingland marked this pull request as ready for review July 22, 2025 02:14
@knative-prow knative-prow bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 22, 2025
@knative-prow knative-prow bot requested a review from nainaz July 22, 2025 02:14
@lkingland lkingland force-pushed the push-tzvwvvkmvwtq branch 13 times, most recently from 5600e62 to e2184c3 Compare July 22, 2025 06:40
@lkingland lkingland marked this pull request as ready for review October 29, 2025 09:03
@knative-prow knative-prow bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 29, 2025
@lkingland
Copy link
Member Author

It ain't perfect, but I would prefer we merge this sooner rather than later as folks need this, and we can open new PRs for any niggling issues.


func (i info) XML(w io.Writer) error {
return xml.NewEncoder(w).Encode(i)
return errors.New("XML export not currently supported")
Copy link
Member Author

Choose a reason for hiding this comment

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

XML breakes trying to export anonymous structs. And XML is evil.

Choose a reason for hiding this comment

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

Maybe it makes sense to remove it here and in format.go?

Comment on lines -12 to -13
* `FUNC_REPO_REF` affects which github repo will be used to fetch tekton tasks for on cluster build. Default: `knative/func`.
* `FUNC_REPO_BRANCH_REF` affects which github branch will be used to fetch tekton tasks for on cluster build. Default: `main`.
Copy link
Member Author

Choose a reason for hiding this comment

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

Apparently these are completely unused.


.PHONY: update-runtime-python
update-runtime-python:
# Nothing to update for Python
Copy link
Member Author

Choose a reason for hiding this comment

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

Explicitly stating why it's not here.

Choose a reason for hiding this comment

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

Is this desired? Why not remove it?


.PHONY: check
check: $(BIN_GOLANGCI_LINT) ## Check code quality (lint)
check: check-lint check-goimports check-misspell check-whitespace check-eof ## Check code quality (comprehensive)
Copy link
Member Author

Choose a reason for hiding this comment

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

Adds checkers to be as close to those from knative automations such that running make check locally is a reasonable approximation.

STERN=$(find_executable "stern")
KN=$(find_executable "kn")
JQ=$(find_executable "jq")
KUBECTL=$(find_executable "kubectl" || true)
Copy link
Member Author

@lkingland lkingland Oct 29, 2025

Choose a reason for hiding this comment

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

not all scripts require all binaries, so this is opportunistic; allowing failure to happen at time of use if not found.

Choose a reason for hiding this comment

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

A short comment here telling exactly that would be great!

@lkingland lkingland force-pushed the push-tzvwvvkmvwtq branch 5 times, most recently from 05ad84d to 4cc4d72 Compare October 29, 2025 11:03
@lkingland lkingland force-pushed the push-tzvwvvkmvwtq branch 2 times, most recently from 4052fcd to 79ef180 Compare October 29, 2025 11:14
install git server in workflow

disable dapr service-to-service test

only run private repo test on Linux

test full script etc

test failure fixes

remote s2i builder test full stuffs

python functions

fixes

update func-python

e2e wait by duration

wait options with extended timeouts for java runtimes

import common in git-server.sh

disable make buffering

import common to dump-logs.sh

update dump-logs.sh to handle missing binaries

update common.sh to be resilient to missing binaries

fix: gitlab tests (retries and skip in CI)

full test target - script

reenable http default matrix template

support user vs system config in registry setup script

e2e_test.go increase default timeout

full test target

add CleanFS option

unified github workflow

renamed env var

standardized envs for int tests

dedicated Podman scripts

integration test name prefix

registry.sh skip docker restart

add missing io import to gitlab_int_test.go

improve prechecks
@knative-prow-robot knative-prow-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 30, 2025
@knative-prow-robot
Copy link

PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Copy link

@twoGiants twoGiants left a comment

Choose a reason for hiding this comment

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

Looks pretty awesome! What a great refactoring! 🥇 😸

Learned a lot about the project reviewing it. And I always like it in PRs when many files are removed and only a few added. It feels more cleaned up then. 🧹 😆 🧹

I have 3 files to review left. Will continue on Monday.

Most, if not all comments are non-blocking.

- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main

- name: Free up disk space

Choose a reason for hiding this comment

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

Nice one! 🥇

This should be a well maintained Github Action...just searched a bit and there are a few, but not all maintained. This one looks maintained.

# Operating system temporary files
.DS_Store

# TODO: Update this test to be from a temp directory with a hard-coded impl:

Choose a reason for hiding this comment

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

I would not commit # TODO: .... Better to create an issue and reference it here, so it can be picked up by someone.

# Issue #1234: ...

Same for the TODO below.

Copy link
Member Author

@lkingland lkingland Nov 3, 2025

Choose a reason for hiding this comment

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

Ideally, yes, but there's an extenuating circumstance: 1) the Knative bot auto-deletes issues after a certain time, and 2) that creates an implicit dependency between our code and the GitHub issues database.

I'm not sure if this is standard practice, but I tend to say:
# TODO: Nonfatal, to be done in the future, possibly (a known, acceptable capitulation)
# FIXME: Fatal, should not be committed until handled.

Choose a reason for hiding this comment

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

Not a fan tbh. It "never" gets picked up and it lacks clarity. Is someone working on it? Should I resolve it? Do I leave it? Whats the priority?

  • Can the Knative bot be configured not to do that if the reference in the code is there?
  • What issue do you see with having this dependency in the code to the Github issues (a tool)?
  • We could also remove the TODO and have an issue in our internal JIRA. Then it won't be deleted and triaged at some point.


.PHONY: update-runtime-python
update-runtime-python:
# Nothing to update for Python

Choose a reason for hiding this comment

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

Is this desired? Why not remove it?

Please note that the version of `yq` required is installed via `pip3 install yq` or `brew install python-yq`


### Allocate

Choose a reason for hiding this comment

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

In the Prerequisites above it should be added that the dapr cli is needed.

The cluster allocation script requires jq, yq, kubectl, kind, dapr, docker and python.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fortunately that's no longer a manual requirement because the ./hack/binaries.sh script now installs all necessary binaries in an isolated environment. That document needs to be (will be) completely rewritten.


func (i info) XML(w io.Writer) error {
return xml.NewEncoder(w).Encode(i)
return errors.New("XML export not currently supported")

Choose a reason for hiding this comment

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

Maybe it makes sense to remove it here and in format.go?


# Install bash 4+ (macOS ships with bash 3.x)
echo "${blue}Installing bash...${reset}"
brew install bash

Choose a reason for hiding this comment

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

Is this really needed on macOS? It ships with zsh as a default since 2019.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it boils down to the hashbang: If we specify zsh, that rules out many linux development machines without zsh (our primary development platform). If we use bash (the universal default), then we need to ensure it's the updated GNU core utils version or our scripts break.

return name
}

func checkTestEnabled(t *testing.T) {

Choose a reason for hiding this comment

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

This function is unused. Also setupNS, createSimpleGoProject. Comment in line 47 can also be removed.

t.Log("created svc:", svc.Name)

// wait for service to start
// TODO: Replace with proper readiness check. This sleep gives the deployment

Choose a reason for hiding this comment

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

I would replace the TODO with a Github issue and reference it here.

// TestInt_Invoke_ServiceToService ensures that a Function can invoke another
// service via localhost service discovery api provided by the Dapr sidecar.
func TestInt_Invoke_ServiceToService(t *testing.T) {
t.Skip("TODO: dapr appears to be borked")

Choose a reason for hiding this comment

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

I would add a Github issue and reference it here and replace the TODO with it.


// Skip in CI due to persistent timeout issues regardless of allocated time
// Note it does indeed run locally.
// TODO: Investigate why GitLab webhook builds are not completing in CI

Choose a reason for hiding this comment

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

I would create a Github issue and reference it here and replace the TODO with it. Then it can be picked up by someone 😸 .

- `./hack/binaries.sh` Fetch binaries into `./hack/bin`
- `./hack/registry.sh` (once) Configure insecure local registriry
- `./hack/cluster.sh` Create a cluster and kube config in `./hack/bin`
- `make test-all` Run all tests using these binaries and cluster
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- `make test-all` Run all tests using these binaries and cluster
- `make test-full` Run all tests using these binaries and cluster


// DefaultNamespace for E2E tests is that used by default in the
// CLI being tested.
DefaultNamespace = cmd.DefaultNamespace
Copy link
Contributor

Choose a reason for hiding this comment

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

This resolves to "default" aways. It would be nice if this could be configurable (i.e. FUNC_E2E_NAMESPACE) or resolve to current namespace on cluster


// Kubeconfig - the kubeconfig to pass ass KUBECONFIG env to test
// environments.
Kubeconfig = getEnvPath("FUNC_E2E_KUBECONFIG", "", DefaultKubeconfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Kubeconfig = getEnvPath("FUNC_E2E_KUBECONFIG", "", DefaultKubeconfig)
Kubeconfig = getEnvPath("FUNC_E2E_KUBECONFIG", "KUBECONFIG", DefaultKubeconfig)

it could also rely on KUBECONFIG env var (just suggestion)

Choose a reason for hiding this comment

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

I thought that for that case the FUNC_E2E_KUBECONFIG should be used.

}
defer l.Close()
return l.Addr().String(), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: what about split this file per test category.

  • e2e_test.go -> consts, init, utils, waitFor funcs,...
  • e2e_core_test.go -> TestCore_*
  • e2e_metadata_test.go -> TestMetadata_*
  • e2e_[remote|podman|matrix]_test.go -> and so on...

Choose a reason for hiding this comment

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

Agree, this would be great!

clean(t, name, DefaultNamespace)
}()

if !waitForEcho(t, fmt.Sprintf("http://%v.default.localtest.me", name)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it could have cluster domain as environment variable either? it would make test more cluster agnostic.

Choose a reason for hiding this comment

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

But the cluster created for this e2e tests is using this cluster domain. Not sure if someone would create/use their own custom setup with a different DNS config just for the tests.

Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes these tests are strict to the Kind cluster created for CI, but I would (I probably will) run these test set with OCP.

if err := newCmd(t, "init", "-l=go").Run(); err != nil {
t.Fatal(err)
}
if err := newCmd(t, "deploy", "--remote", "--builder=pack", "--registry=registry.default.svc.cluster.local:5000/func").Run(); err != nil {
Copy link
Contributor

@jrangelramos jrangelramos Oct 31, 2025

Choose a reason for hiding this comment

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

Registry is hardcoded here (there are other occurencies). Maybe this could be a variable (i.e ClusterInternalRegistry) also configurable by an env var?

Copy link

@twoGiants twoGiants left a comment

Choose a reason for hiding this comment

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

Great job 😸 🚀 🎸 🥇!

This is a big improvement to what is there and the structure is nice, clean and clear.

I haven't found anything critical. Just typos, TODOs which can be extracted into issues and a few minor nice-to-haves.

I am mostly done. The last file I am still reviewing are the e2e-test.go which is the biggest one. I will submit what I have so far for it and the rest today/tomorrow.

debuggable) locally by a developer, in addition to remotely in CI as
acceptance criteria for pull requests.

## Runnning E2Es locally: a Quick-start

Choose a reason for hiding this comment

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

Suggested change
## Runnning E2Es locally: a Quick-start
## Running E2Es locally: a Quick-start

## Runnning E2Es locally: a Quick-start

- `./hack/binaries.sh` Fetch binaries into `./hack/bin`
- `./hack/registry.sh` (once) Configure insecure local registriry

Choose a reason for hiding this comment

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

Suggested change
- `./hack/registry.sh` (once) Configure insecure local registriry
- `./hack/registry.sh` (once) Configure insecure local registry

- `binaries.sh`: Installs executables needed for cluster setup and
configuration into hack/bin.

- `registry.sh`: Configures the local Podman or Docker to allow unencrypted

Choose a reason for hiding this comment

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

Should there be also a --revert or --undo option for this script?

package, not the directory from which `go test` was run.

`FUNC_E2E_PLUGIN`: if set, the command run by the tests will be
`${FUNC_E2E_BIN} func`, allowing for running all tests when func is installed

Choose a reason for hiding this comment

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

Suggested change
`${FUNC_E2E_BIN} func`, allowing for running all tests when func is installed
`${FUNC_E2E_BIN} ${FUNC_E2E_PLUGIN}`, allowing for running all tests when func is installed


## Running

From the root of the repository, run `make test-all`. This will compile

Choose a reason for hiding this comment

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

From the root of the repository, run make test-full. This will compile

}
}

// waitForEcho returns true if there is service at the given addresss which

Choose a reason for hiding this comment

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

Suggested change
// waitForEcho returns true if there is service at the given addresss which
// waitForEcho returns true if there is service at the given address which

// If the Function returns a 500, it is considered a positive test failure
// by the implementation and retries are discontinued.
//
// TODO: Implement a --output=json flag on `func run` and update all

Choose a reason for hiding this comment

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

I would create a Github issue for this TODO and reference it here.

for time.Since(startTime) < cfg.timeout {
attemptCount++
time.Sleep(cfg.interval)
// t.Logf("GET %v\n", address)

Choose a reason for hiding this comment

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

Remove or uncomment.

Suggested change
// t.Logf("GET %v\n", address)
t.Logf("GET %v\n", address)

}

// Wait for echo
if !waitForEcho(t, "http://"+address) {

Choose a reason for hiding this comment

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

Does this run into flakes a lot? If not then all good!

If yes, then a more specific implementation using kubectl which waits for the pod and service availability and then sends an echo would be an improvement.

But not here in this PR of course. It can be a help-wanted issue.

clean(t, name, DefaultNamespace)
}()

if !waitForEcho(t, fmt.Sprintf("http://%v.default.localtest.me", name)) {

Choose a reason for hiding this comment

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

But the cluster created for this e2e tests is using this cluster domain. Not sure if someone would create/use their own custom setup with a different DNS config just for the tests.

Wdyt?

Copy link

@twoGiants twoGiants left a comment

Choose a reason for hiding this comment

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

Hi @lkingland! Finished the e2e_test.go. That was the biggest one! 😅

Looks good to me!

I have only found some typos and propose to cleanup a bit here and there, extract issues from TODOs and reduce duplication. Can all be done in a follow up, except the TODOs, those I would remove now and create issues. But you decide, its all good 😸

}

// Assert
f, err := fn.NewFunction(root)

Choose a reason for hiding this comment

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

I would probably assert a bit more here. Because it's an e2e test I would check that what I expect was actually generated and has what I expect in it.

}

// TestCore_Deploy_Template ensures that the system supports creating
// functions based off templates in a remote repository.

Choose a reason for hiding this comment

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

Suggested change
// functions based off templates in a remote repository.
// functions based of templates in a remote repository.

// repo: github.com/functions-dev
// runtime: go
// template: http (the default. can be changed with --template)
if err := newCmd(t, "init", "-l=go", "--repository=https://github.com/functions-dev/func-e2e-tests").Run(); err != nil {

Choose a reason for hiding this comment

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

https://github.com/functions-dev/func-e2e-tests => hosted on the gitlab instance in the bootstrapped cluster would be nice and more independent 😸 . Maybe also a help-wanted issue for later.

Copy link
Contributor

Choose a reason for hiding this comment

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

It could be even be hosted on the git-server available for remote tests.

}

// Validate that the name matches what we expect
if instance.Name != name {

Choose a reason for hiding this comment

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

Describe shows more than just the name. Maybe we could assert for the entire content?

}
}()

// TODO: complete implementation of `func run --json` structured output

Choose a reason for hiding this comment

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

I would put the TODO in a Github issue and reference it here.

Comment on lines +2280 to +2283
if err == nil {
return false // no error is not an abnormal error.
}
t.Helper()

Choose a reason for hiding this comment

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

Suggested change
if err == nil {
return false // no error is not an abnormal error.
}
t.Helper()
t.Helper()
if err == nil {
return false // no error is not an abnormal error.
}


// setSecret creates or replaces a secret.
func setSecret(t *testing.T, name, ns string, data map[string][]byte) {
ctx := context.Background()

Choose a reason for hiding this comment

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

Suggested change
ctx := context.Background()
t.Helper()
ctx := context.Background()


// setConfigMap creates or replaces a configMap
func setConfigMap(t *testing.T, name, ns string, data map[string]string) {
ctx := context.Background()

Choose a reason for hiding this comment

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

Suggested change
ctx := context.Background()
t.Helper()
ctx := context.Background()

// There is currently a bug in delete which hangs for several seconds
// when deleting a Function. This adds considerably to the test suite
// execution time. Tests are written such that they are not dependent
// on a clean exit/cleanup, so this step is skipped for speed.

Choose a reason for hiding this comment

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

The comment is a bit misleading: ..., so this step is skipped for speed. Where is this step skipped?

func logImageStats(t *testing.T, name string, phase string) {
t.Helper()

// Get image size for this specific function

Choose a reason for hiding this comment

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

Suggested change
// Get image size for this specific function
// Get image name for this specific function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants