Skip to content
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

Reintroduce parallel exec 72658 #83

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2cde27c
Moving fill-in of OAuth Client field out of template functions
Jun 6, 2024
4c2e404
datastore: Using RWMutex (also for reading/writing responses)
Jun 6, 2024
4a854bc
Re-introducing tests for parallel execution, modified for new require…
Jun 6, 2024
6d391cc
rewriting old path_utils based on new path syntax
Jun 6, 2024
261cde5
Re-introducing Parallel Execution, in a simplified version
Jun 7, 2024
feba700
Parallel Exec: No longer forcing ContinueOnFailure
Jun 7, 2024
b84c527
Parallel Exec: Working with WaitGroup and atomic.Uint32 instead of ch…
Jun 7, 2024
8496fc4
wording: repetitions -> parallel runs
Jun 7, 2024
61d734e
file_test: Removing unused struct left over from moving out path_util…
Jun 7, 2024
d3517d9
path_util: Removing outdated comment, added explanation for pathSpecR…
Jun 7, 2024
9413bff
path_util: Removing support for quoted strings
Jun 7, 2024
b8e281b
Simplified ParsePathSpec to not use a regexp (more readable now)
martinrode Jun 7, 2024
ee47406
path_util: Adding test cases for zero/negative parallel runs
Jun 7, 2024
982a026
path_util: Changing ParsePathSpec to return error instead of bool
Jun 7, 2024
33bed31
path_util: Allowing ParallelRuns=0
Jun 7, 2024
40bba7a
api_testsuite: naming repeatIdx -> runIdx
Jun 7, 2024
259dd17
Adding parallel_run_idx template function
Jun 7, 2024
7f69f52
Adding README section about parallel_run_idx template function
Jun 7, 2024
eee7842
Moving template rendering into Goroutine + fixes to parallel_run_idx
Jun 7, 2024
0154ef8
Moving test goroutine into separate function for readability
Jun 7, 2024
f56b293
combining redundant if statements
Jun 7, 2024
d2e2f3b
parseAndRunTest / testGoroutine: Simplifying testFilePath / manifestDir
Jun 7, 2024
a51fab2
ParsePathSpec: Returning *PathSpec instead of PathSpec
Jun 7, 2024
8fa7163
Removing tabs in test JSONs
Jun 7, 2024
beb5c40
OpenFileOrUrl: Clarifying purpose and error handling of ParsePathSpec…
Jun 17, 2024
e964955
ParsePathSpec: Using var block
Jun 17, 2024
eafc0ee
OpenFileOrUrl: Moving PathSpec parsing to caller side
Jun 17, 2024
5a0c57c
BuildPolicy: Using io.Reader and io.Closer to ensure file handle can …
Jun 17, 2024
7ba5b47
Fixing defer in for
Jun 17, 2024
a01c29a
Moving loadFileFromPathSpec to PathSpec.LoadContents
Jun 17, 2024
6e37bb3
Refactoring LoadManifest* functions to return PathSpec instead of string
Jun 17, 2024
b6fb2e1
Removing Closer from build_policies
Jun 19, 2024
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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,13 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
// [SINGLE TESTCASE]: See below for more information

// We also support the external loading of a complete test:
"@pathToTest.json"
"@pathToTest.json",

// By prefixing it with a number, the testtool runs that many instances of
// the included test file in parallel to each other.
//
// Only tests directly included by the manifest are allowed to run in parallel.
"5@pathToTestsThatShouldRunInParallel.json"
]
}
```
Expand All @@ -160,8 +166,9 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
### manifest.json
```yaml
{
// Define if the testuite should continue even if this test fails. (default:false)
// Define if the test suite should continue even if this test fails. (default: false)
"continue_on_failure": true,

// Name to identify this single test. Is important for the log. Try to give an explaning name
"name": "Testname",

Expand Down Expand Up @@ -413,6 +420,31 @@ However that one will be stripped before parsing the template, which would be ju

** Unlike with delimiters, external tests/requests/responses don't inherit those removals, and need to be specified per file.

## Run tests in parallel
The tool is able to run tests in parallel to themselves. You activate this
mechanism by including an external test file with `N@pathtofile.json`, where N
is the number of parallel "clones" you want to have of the included tests.

The included tests themselves are still run serially, only the entire set of
tests will run in parallel for the specified number of replications.

This is useful e.g. for stress-testing an API.

Only tests directly included by a manifest are allowed to run in parallel.

```yaml
{
"name": "Binary Comparison",
"request": {
"endpoint": "suggest",
"method": "GET"
},

// Path to binary file with N@
"response": "123@simple.bin"
}
```

## Binary data comparison

The tool is able to do a comparison with a binary file. Here we take a MD5 hash of the file and and then later compare
Expand Down
164 changes: 114 additions & 50 deletions api_testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"path/filepath"
"strconv"
"sync"
"sync/atomic"
"time"

"github.com/pkg/errors"
Expand Down Expand Up @@ -45,7 +47,7 @@ type Suite struct {
manifestPath string
reporterRoot *report.ReportElement
index int
serverURL string
serverURL *url.URL
httpServer http.Server
httpServerProxy *httpproxy.Proxy
httpServerDir string
Expand Down Expand Up @@ -149,6 +151,12 @@ func NewTestSuite(config TestToolConfig, manifestPath string, manifestDir string
//Append suite manifest path to name, so we know in an automatic setup where the test is loaded from
suite.Name = fmt.Sprintf("%s (%s)", suite.Name, manifestPath)

// Parse serverURL
suite.serverURL, err = url.Parse(suite.Config.ServerURL)
if err != nil {
return nil, fmt.Errorf("can not load server url : %s", err)
}

// init store
err = suite.datastore.SetMap(suite.Store)
if err != nil {
Expand Down Expand Up @@ -179,8 +187,18 @@ func (ats *Suite) Run() bool {
success := true
for k, v := range ats.Tests {
child := r.NewChild(strconv.Itoa(k))
sTestSuccess := ats.parseAndRunTest(v, ats.manifestDir, ats.manifestPath, child, ats.loader)

sTestSuccess := ats.parseAndRunTest(
v,
ats.manifestDir,
ats.manifestPath,
child,
ats.buildLoader(ats.loader, -1),
true, // parallel exec allowed for top-level tests
)

child.Leave(sTestSuccess)

if !sTestSuccess {
success = false
break
Expand Down Expand Up @@ -213,21 +231,43 @@ type TestContainer struct {
Path string
}

func (ats *Suite) parseAndRunTest(v any, manifestDir, testFilePath string, r *report.ReportElement, rootLoader template.Loader) bool {
//Init variables
// logrus.Warnf("Test %s, Prev delimiters: %#v", testFilePath, rootLoader.Delimiters)
func (ats *Suite) buildLoader(rootLoader template.Loader, parallelRunIdx int) template.Loader {
loader := template.NewLoader(ats.datastore)
loader.Delimiters = rootLoader.Delimiters
loader.HTTPServerHost = ats.HTTPServerHost
serverURL, err := url.Parse(ats.Config.ServerURL)
if err != nil {
logrus.Error(fmt.Errorf("can not load server url into test (%s): %s", testFilePath, err))
return false
}
loader.ServerURL = serverURL
loader.ServerURL = ats.serverURL
loader.OAuthClient = ats.Config.OAuthClient
loader.ParallelRunIdx = parallelRunIdx

return loader
}

//Get the Manifest with @ logic
func (ats *Suite) parseAndRunTest(
v any, manifestDir, testFilePath string, r *report.ReportElement,
loader template.Loader, allowParallelExec bool,
) bool {
// Parse PathSpec (if any) and determine number of parallel runs
parallelRuns := 1
if vStr, ok := v.(string); ok {
pathSpec, err := util.ParsePathSpec(vStr)
if err != nil {
logrus.Error(fmt.Errorf("test string is not a valid path spec: %w", err))
return false
}

parallelRuns = pathSpec.ParallelRuns
}

// Configure parallel runs, if specified
if parallelRuns > 1 {
// Check that parallel runs are actually allowed
if !allowParallelExec {
logrus.Error(fmt.Errorf("parallel runs are not allowed in nested tests (%s)", testFilePath))
return false
}
}

// Get the Manifest with @ logic
fileh, testObj, err := template.LoadManifestDataAsRawJson(v, manifestDir)
dir := filepath.Dir(fileh)
if fileh != "" {
Expand Down Expand Up @@ -265,45 +305,69 @@ func (ats *Suite) parseAndRunTest(v any, manifestDir, testFilePath string, r *re
}

// Execute test cases
for i, testCase := range testCases {
var success bool

// If testCase can be unmarshalled as string, we may have a
// reference to another test using @ notation at hand
var testCaseStr string
err = util.Unmarshal(testCase, &testCaseStr)
if err == nil && util.IsPathSpec(testCaseStr) {
// Recurse if the testCase points to another file using @ notation
success = ats.parseAndRunTest(
testCaseStr,
filepath.Join(manifestDir, dir),
testFilePath,
r,
loader,
)
} else {
// Otherwise simply run the literal test case
success = ats.runLiteralTest(
TestContainer{
CaseByte: testCase,
Path: filepath.Join(manifestDir, dir),
},
r,
testFilePath,
loader,
i,
)
}

if !success {
return false
}
}

return true
var successCount atomic.Uint32
var waitGroup sync.WaitGroup

waitGroup.Add(parallelRuns)

for runIdx := range parallelRuns {
runIdxCapture := runIdx

go func() {
defer waitGroup.Done()

for testIdx, testCase := range testCases {
var success bool

caseLoader := ats.buildLoader(loader, runIdxCapture)

lhinderberger marked this conversation as resolved.
Show resolved Hide resolved
// If testCase can be unmarshalled as string, we may have a
// reference to another test using @ notation at hand
var testCaseStr string
err = util.Unmarshal(testCase, &testCaseStr)
if err == nil && util.IsPathSpec(testCaseStr) {
// Recurse if the testCase points to another file using @ notation
success = ats.parseAndRunTest(
testCaseStr,
filepath.Join(manifestDir, dir),
testFilePath,
r,
caseLoader,
false, // no parallel exec allowed in nested tests
)
} else {
// Otherwise simply run the literal test case
success = ats.runLiteralTest(
TestContainer{
CaseByte: testCase,
Path: filepath.Join(manifestDir, dir),
},
r,
testFilePath,
caseLoader,
runIdxCapture*len(testCases)+testIdx,
)
}

if !success {
// note that successCount is not incremented
return
}
}

successCount.Add(1)
}()
}

waitGroup.Wait()

return successCount.Load() == uint32(parallelRuns)
}

func (ats *Suite) runLiteralTest(tc TestContainer, r *report.ReportElement, testFilePath string, loader template.Loader, k int) bool {
func (ats *Suite) runLiteralTest(
tc TestContainer, r *report.ReportElement, testFilePath string, loader template.Loader,
index int,
) bool {
r.SetName(testFilePath)

var test Case
Expand All @@ -320,7 +384,7 @@ func (ats *Suite) runLiteralTest(tc TestContainer, r *report.ReportElement, test
test.loader = loader
test.manifestDir = tc.Path
test.suiteIndex = ats.index
test.index = k
test.index = index
test.dataStore = ats.datastore
test.standardHeader = ats.StandardHeader
test.standardHeaderFromStore = ats.StandardHeaderFromStore
Expand Down
14 changes: 14 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ func NewTestToolConfig(serverURL string, rootDirectory []string, logNetwork bool
LogShort: logShort,
OAuthClient: Config.Apitest.OAuthClient,
}

config.fillInOAuthClientNames()

err = config.extractTestDirectories()
return config, err
}
Expand Down Expand Up @@ -116,3 +119,14 @@ func (config *TestToolConfig) extractTestDirectories() error {
}
return nil
}

// fillInOAuthClientNames fills in the Client field of loaded OAuthClientConfig
// structs, which the user may have left unset in the config yaml file.
func (config *TestToolConfig) fillInOAuthClientNames() {
for key, clientConfig := range config.OAuthClient {
if clientConfig.Client == "" {
clientConfig.Client = key
config.OAuthClient[key] = clientConfig
}
}
}
13 changes: 11 additions & 2 deletions pkg/lib/datastore/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ type Datastore struct {
storage map[string]any // custom storage
responseJson []string // store the responses
logDatastore bool
lock *sync.Mutex
lock *sync.RWMutex
}

func NewStore(logDatastore bool) *Datastore {
ds := Datastore{}
ds.storage = make(map[string]any, 0)
ds.responseJson = make([]string, 0)
ds.logDatastore = logDatastore
ds.lock = &sync.Mutex{}
ds.lock = &sync.RWMutex{}
return &ds
}

Expand Down Expand Up @@ -84,11 +84,17 @@ func (ds *Datastore) Delete(k string) {

// We store the response
func (ds *Datastore) UpdateLastResponse(s string) {
ds.lock.Lock()
defer ds.lock.Unlock()

ds.responseJson[len(ds.responseJson)-1] = s
}

// We store the response
func (ds *Datastore) AppendResponse(s string) {
ds.lock.Lock()
defer ds.lock.Unlock()

ds.responseJson = append(ds.responseJson, s)
}

Expand Down Expand Up @@ -167,6 +173,9 @@ func (ds *Datastore) Set(index string, value any) error {
}

func (ds Datastore) Get(index string) (res any, err error) {
ds.lock.RLock()
defer ds.lock.RUnlock()

// strings are evalulated as int, so
// that we can support "-<int>" notations

Expand Down
Loading
Loading