-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test(e2e): add E2E testing utilities and sample specs #639
Changes from all commits
7844882
f76783b
978219a
72dd79d
dccb967
dc2c8d6
8876a01
1b86ea3
e9aa7ad
b3c5206
8fe7427
66661ab
88d2623
463cc60
4cd813f
bbf3fc0
3f32cd1
c2dd82a
14c0e64
d6007c0
e3a0305
41e9e26
8ab1b59
87d0d53
635215b
0a0ef4a
83f02a0
0c2bd61
479ca54
e6b1ba6
bed64a9
cf8ff9e
3c14379
4806422
c269f12
c2fdbd8
74ccab3
d242da2
06fe51f
0992729
c27309b
0f85baa
cb93b2e
2c24798
7fa6838
248c4e5
6b0848d
8170b50
37651db
a3e595f
747f8fd
f991754
1aab431
c3a92f7
2324232
b069bbd
9cbd970
34ddf67
4af75dc
6087fb2
09068f5
f2c62ab
6ac4417
1eab9e6
95aed09
01def2a
6757c23
ce2b308
6bc755b
737f34f
db69f49
7984b40
1be77aa
b29535f
1e26b37
cb3e9fa
4745024
9faf62f
cbc8890
30ae078
3fc5876
315708c
da1d5fb
141a491
81a8c1d
cdffa27
d979fba
cf84ed1
a753585
4769e8d
176411e
2dc6684
705ecb4
e5560eb
fab5699
9fe2b3d
ffd8c3f
6f26be5
177725b
6e9e971
7365e58
35be2dc
432da50
6acbe3f
f439e1b
a0d7559
6f650cc
1a4cb3a
a2dc628
6c30fd3
2255a02
47ee168
eba0431
fcc3828
a2f9ff5
e02cc86
98f5e03
9ba0a17
456a3ba
eea1fb3
ed9af5b
c0a09bb
1b0fa11
0d24bac
29c24b3
19be1f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,18 +44,21 @@ jobs: | |
- name: Run E2E Tests | ||
run: | | ||
cd $GITHUB_WORKSPACE/test/e2e | ||
mnt_root="$GITHUB_WORKSPACE/test/e2e/testdata/distribution/mount" | ||
go install github.com/onsi/ginkgo/v2/ginkgo | ||
mnt_root="$GITHUB_WORKSPACE/test/e2e/testdata/distribution/mount" | ||
rm -rf $mnt_root/docker | ||
for layer in $(ls $mnt_root/*.tar.gz); do | ||
tar -xvzf $layer -C $mnt_root | ||
done | ||
trap 'docker kill oras-e2e || true' ERR | ||
docker run -d -p 5000:5000 --rm --name oras-e2e \ | ||
--env STORAGE_DELETE_ENABLED=true \ | ||
docker run --pull always -d -p 5000:5000 --rm --name oras-e2e \ | ||
--env REGISTRY_STORAGE_DELETE_ENABLED=true \ | ||
--env REGISTRY_AUTH_HTPASSWD_REALM=test-basic \ | ||
--env REGISTRY_AUTH_HTPASSWD_PATH=/etc/docker/registry/passwd \ | ||
--mount type=bind,source=$mnt_root/docker,target=/opt/data/registry-root-dir/docker \ | ||
--mount type=bind,source=$mnt_root/passwd_bcrypt,target=/etc/docker/registry/passwd \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we generate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's doable but will complicate the setup. Can we set this aside and consider the design in another issue #680 with configurable username/password/token? |
||
ghcr.io/oras-project/registry:v1.0.0-rc.2 | ||
ginkgo -r -p suite | ||
ginkgo -r -p --succinct suite | ||
docker kill oras-e2e || true | ||
env: | ||
ORAS_PATH: bin/linux/amd64/oras | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
/* | ||
Copyright The ORAS Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package utils | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
"time" | ||
|
||
ginkgo "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
"github.com/onsi/gomega/gexec" | ||
"oras.land/oras/test/e2e/internal/utils/match" | ||
) | ||
|
||
const ( | ||
orasBinary = "oras" | ||
|
||
// customize your own basic auth file via `htpasswd -cBb <file_name> <user_name> <password>` | ||
Username = "hello" | ||
Password = "oras-test" | ||
AuthConfigPath = "test.config" | ||
) | ||
|
||
// ExecOption provides option used to execute a command. | ||
type ExecOption struct { | ||
binary string | ||
args []string | ||
workDir string | ||
timeout time.Duration | ||
|
||
stdin io.Reader | ||
stdout []match.Matcher | ||
stderr []match.Matcher | ||
shouldFail bool | ||
qweeah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
text string | ||
} | ||
|
||
// ORAS returns default execution option for oras binary. | ||
func ORAS(args ...string) *ExecOption { | ||
return Binary(orasBinary, args...) | ||
} | ||
|
||
// Binary returns default execution option for customized binary. | ||
func Binary(path string, args ...string) *ExecOption { | ||
return &ExecOption{ | ||
binary: path, | ||
args: args, | ||
timeout: 10 * time.Second, | ||
shouldFail: false, | ||
} | ||
} | ||
|
||
// WithFailureCheck sets failure exit code checking for the execution. | ||
func (opts *ExecOption) WithFailureCheck() *ExecOption { | ||
opts.shouldFail = true | ||
return opts | ||
} | ||
|
||
// WithTimeOut sets timeout for the execution. | ||
func (opts *ExecOption) WithTimeOut(timeout time.Duration) *ExecOption { | ||
opts.timeout = timeout | ||
return opts | ||
} | ||
|
||
// WithDescription sets description text for the execution. | ||
func (opts *ExecOption) WithDescription(text string) *ExecOption { | ||
opts.text = text | ||
return opts | ||
} | ||
|
||
// WithWorkDir sets working directory for the execution. | ||
func (opts *ExecOption) WithWorkDir(path string) *ExecOption { | ||
opts.workDir = path | ||
return opts | ||
} | ||
|
||
// WithInput redirects stdin to r for the execution. | ||
func (opts *ExecOption) WithInput(r io.Reader) *ExecOption { | ||
opts.stdin = r | ||
return opts | ||
} | ||
|
||
// MatchKeyWords adds keywords matching to stdout. | ||
func (opts *ExecOption) MatchKeyWords(keywords ...string) *ExecOption { | ||
opts.stdout = append(opts.stdout, match.NewKeywordMatcher(keywords)) | ||
return opts | ||
} | ||
|
||
// MatchErrKeyWords adds keywords matching to stderr. | ||
func (opts *ExecOption) MatchErrKeyWords(keywords ...string) *ExecOption { | ||
opts.stderr = append(opts.stderr, match.NewKeywordMatcher(keywords)) | ||
return opts | ||
} | ||
|
||
// MatchContent adds full content matching to the execution. | ||
func (opts *ExecOption) MatchContent(content string) *ExecOption { | ||
if !opts.shouldFail { | ||
opts.stdout = append(opts.stdout, match.NewContentMatcher(content)) | ||
} else { | ||
opts.stderr = append(opts.stderr, match.NewContentMatcher(content)) | ||
} | ||
return opts | ||
} | ||
|
||
// MatchStatus adds full content matching to the execution option. | ||
func (opts *ExecOption) MatchStatus(keys []match.StateKey, verbose bool, successCount int) *ExecOption { | ||
opts.stdout = append(opts.stdout, match.NewStatusMatcher(keys, opts.args[0], verbose, successCount)) | ||
return opts | ||
} | ||
|
||
// Exec run the execution based on opts. | ||
func (opts *ExecOption) Exec() *gexec.Session { | ||
if opts == nil { | ||
// this should be a code error but can only be caught during runtime | ||
panic("Nil option for command execution") | ||
} | ||
|
||
if opts.text == "" { | ||
if opts.shouldFail { | ||
opts.text = "fail" | ||
} else { | ||
opts.text = "pass" | ||
} | ||
} | ||
description := fmt.Sprintf(">> should %s: %s %s >>", opts.text, opts.binary, strings.Join(opts.args, " ")) | ||
ginkgo.By(description) | ||
|
||
var cmd *exec.Cmd | ||
if opts.binary == orasBinary { | ||
opts.binary = ORASPath | ||
} | ||
cmd = exec.Command(opts.binary, opts.args...) | ||
cmd.Stdin = opts.stdin | ||
if opts.workDir != "" { | ||
// switch working directory | ||
wd, err := os.Getwd() | ||
Expect(err).ShouldNot(HaveOccurred()) | ||
Expect(os.Chdir(opts.workDir)).ShouldNot(HaveOccurred()) | ||
defer os.Chdir(wd) | ||
} | ||
fmt.Println(description) | ||
session, err := gexec.Start(cmd, os.Stdout, os.Stderr) | ||
Expect(err).ShouldNot(HaveOccurred()) | ||
Expect(session.Wait(opts.timeout).ExitCode() != 0).Should(Equal(opts.shouldFail)) | ||
|
||
// matching result | ||
for _, s := range opts.stdout { | ||
s.Match(session.Out) | ||
} | ||
for _, s := range opts.stderr { | ||
s.Match(session.Err) | ||
} | ||
|
||
return session | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
Copyright The ORAS Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package utils | ||
|
||
import ( | ||
"io" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
) | ||
|
||
var testFileRoot string | ||
|
||
// CopyTestData copies test data into the temp test folder. | ||
func CopyTestData(dstRoot string) error { | ||
return filepath.WalkDir(testFileRoot, func(path string, d fs.DirEntry, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
if d.IsDir() { | ||
qweeah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// ignore folder | ||
return nil | ||
} | ||
|
||
relPath, err := filepath.Rel(testFileRoot, path) | ||
if err != nil { | ||
return err | ||
} | ||
dstPath := filepath.Join(dstRoot, relPath) | ||
// make sure all parents are created | ||
if err := os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil { | ||
return err | ||
} | ||
|
||
// copy with original folder structure | ||
return copyFile(path, dstPath) | ||
}) | ||
} | ||
|
||
func copyFile(srcFile, dstFile string) error { | ||
to, err := os.Create(dstFile) | ||
if err != nil { | ||
return err | ||
} | ||
defer to.Close() | ||
|
||
from, err := os.Open(srcFile) | ||
if err != nil { | ||
return err | ||
} | ||
defer from.Close() | ||
|
||
_, err = io.Copy(to, from) | ||
return err | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
Copyright The ORAS Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package match | ||
|
||
import ( | ||
. "github.com/onsi/gomega" | ||
"github.com/onsi/gomega/gbytes" | ||
) | ||
|
||
// contentMatcher provides whole matching of the output. | ||
type contentMatcher string | ||
|
||
// NewContentMatcher returns a content matcher. | ||
func NewContentMatcher(s string) contentMatcher { | ||
return contentMatcher(s) | ||
} | ||
|
||
// Match matches got with s. | ||
func (c contentMatcher) Match(got *gbytes.Buffer) { | ||
content := got.Contents() | ||
Expect(contentMatcher(content)).Should(Equal(c)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to
--pull always
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default behavior is
missing
, which can be wrong if github provided an uncleaned VM.always
is more defensive.