• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

UiPath / uipathcli / 14427696216

13 Apr 2025 08:16AM UTC coverage: 90.076% (-0.4%) from 90.463%
14427696216

push

github

thschmitt
Add support to run tests from multiple sources in parallel

Extended the existing `uipath studio test run` command to support
testing multiple projects in parallel.

The `--source` parameter takes now a comma-separated list of project
paths, packages all projects in parallel and kicks off the test runs in
orchestrator.

Fixed bug in the `type_converter` where the backslash character was
dropped even when it was not used to escape the separator.

Added MultiLogger so that parallel debug log output is prefixed with
[1], [2], etc... and can be correlated.

Added registering ResponseHandler in the tests which is more flexible
than the next response API and allows returning different results based
on some request data or test state. It is used to count the number of
calls and to return different results based on the request body.

142 of 190 new or added lines in 8 files covered. (74.74%)

2 existing lines in 1 file now uncovered.

6190 of 6872 relevant lines covered (90.08%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.6
/plugin/studio/test_run_command.go
1
package studio
2

3
import (
4
        "bytes"
5
        "crypto/rand"
6
        "encoding/json"
7
        "fmt"
8
        "math"
9
        "math/big"
10
        "net/url"
11
        "os"
12
        "path/filepath"
13
        "strconv"
14
        "strings"
15
        "sync"
16
        "time"
17

18
        "github.com/UiPath/uipathcli/log"
19
        "github.com/UiPath/uipathcli/output"
20
        "github.com/UiPath/uipathcli/plugin"
21
        "github.com/UiPath/uipathcli/utils/api"
22
        "github.com/UiPath/uipathcli/utils/directories"
23
        "github.com/UiPath/uipathcli/utils/process"
24
        "github.com/UiPath/uipathcli/utils/stream"
25
        "github.com/UiPath/uipathcli/utils/visualization"
26
)
27

28
// The TestRunCommand packs a project as a test package,
29
// uploads it to the connected Orchestrator instances
30
// and runs the tests.
31
type TestRunCommand struct {
32
        Exec process.ExecProcess
33
}
34

35
func (c TestRunCommand) Command() plugin.Command {
1✔
36
        return *plugin.NewCommand("studio").
1✔
37
                WithCategory("test", "Test", "Tests your UiPath studio packages").
1✔
38
                WithOperation("run", "Run Tests", "Tests a given package").
1✔
39
                WithParameter("source", plugin.ParameterTypeStringArray, "Path to one or more project.json files or folders containing project.json files (default: .)", false).
1✔
40
                WithParameter("timeout", plugin.ParameterTypeInteger, "Time to wait in seconds for tests to finish (default: 3600)", false)
1✔
41
}
1✔
42

43
func (c TestRunCommand) Execute(ctx plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
44
        sources, err := c.getSources(ctx)
1✔
45
        if err != nil {
2✔
46
                return err
1✔
47
        }
1✔
48
        timeout := time.Duration(c.getIntParameter("timeout", 3600, ctx.Parameters)) * time.Second
1✔
49

1✔
50
        params, err := c.prepareExecution(sources, timeout, logger)
1✔
51
        if err != nil {
1✔
NEW
52
                return err
×
NEW
53
        }
×
54
        result, err := c.executeAll(params, ctx, logger)
1✔
55
        if err != nil {
2✔
56
                return err
1✔
57
        }
1✔
58

59
        json, err := json.Marshal(result)
1✔
60
        if err != nil {
1✔
61
                return fmt.Errorf("pack command failed: %v", err)
×
62
        }
×
63
        return writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
1✔
64
}
65

66
func (c TestRunCommand) prepareExecution(sources []string, timeout time.Duration, logger log.Logger) ([]testRunParams, error) {
1✔
67
        tmp, err := directories.Temp()
1✔
68
        if err != nil {
1✔
69
                return nil, err
×
70
        }
×
71

72
        params := []testRunParams{}
1✔
73
        for i, source := range sources {
2✔
74
                projectReader := newStudioProjectReader(source)
1✔
75
                project, err := projectReader.ReadMetadata()
1✔
76
                if err != nil {
1✔
NEW
77
                        return nil, err
×
NEW
78
                }
×
79
                supported, err := project.TargetFramework.IsSupported()
1✔
80
                if !supported {
1✔
NEW
81
                        return nil, err
×
NEW
82
                }
×
83

84
                executionLogger := logger
1✔
85
                if len(sources) > 1 {
1✔
NEW
86
                        executionLogger = NewMultiLogger(logger, "["+strconv.Itoa(i+1)+"] ")
×
NEW
87
                }
×
88
                uipcli := newUipcli(c.Exec, executionLogger)
1✔
89
                err = uipcli.Initialize(project.TargetFramework)
1✔
90
                if err != nil {
1✔
NEW
91
                        return nil, err
×
NEW
92
                }
×
93
                destination := filepath.Join(tmp, c.randomTestRunFolderName())
1✔
94
                params = append(params, *newTestRunParams(i, uipcli, executionLogger, source, destination, timeout))
1✔
95
        }
96
        return params, nil
1✔
97
}
98

99
func (c TestRunCommand) executeAll(params []testRunParams, ctx plugin.ExecutionContext, logger log.Logger) (*testRunReport, error) {
1✔
100
        statusChannel := make(chan testRunStatus)
1✔
101
        var wg sync.WaitGroup
1✔
102
        for _, p := range params {
2✔
103
                wg.Add(1)
1✔
104
                go c.execute(p, ctx, p.Logger, &wg, statusChannel)
1✔
105
        }
1✔
106

107
        go func() {
2✔
108
                wg.Wait()
1✔
109
                close(statusChannel)
1✔
110
        }()
1✔
111

112
        var progressBar *visualization.ProgressBar
1✔
113
        if !ctx.Debug {
2✔
114
                progressBar = visualization.NewProgressBar(logger)
1✔
115
                defer progressBar.Remove()
1✔
116
        }
1✔
117
        once := sync.Once{}
1✔
118
        progress := c.showPackagingProgress(progressBar)
1✔
119
        defer once.Do(func() { close(progress) })
1✔
120

121
        status := make([]testRunStatus, len(params))
1✔
122
        for s := range statusChannel {
2✔
123
                once.Do(func() { close(progress) })
2✔
124
                status[s.ExecutionId] = s
1✔
125
                c.updateProgressBar(progressBar, status)
1✔
126
        }
127

128
        results := []testRunResult{}
1✔
129
        for _, s := range status {
2✔
130
                if s.Err != nil {
2✔
131
                        return nil, s.Err
1✔
132
                }
1✔
133
                results = append(results, *s.Result)
1✔
134
        }
135
        return newTestRunReport(results), nil
1✔
136
}
137

138
func (c TestRunCommand) updateProgressBar(progressBar *visualization.ProgressBar, status []testRunStatus) {
1✔
139
        if progressBar == nil {
1✔
NEW
140
                return
×
NEW
141
        }
×
142
        state, totalTests, completedTests := c.calculateOverallProgress(status)
1✔
143
        if state == TestRunStatusUploading {
2✔
144
                progressBar.UpdatePercentage("uploading...", 0)
1✔
145
        } else if state == TestRunStatusRunning && totalTests == 0 && completedTests == 0 {
2✔
NEW
146
                progressBar.UpdatePercentage("running...  ", 0)
×
147
        } else if state == TestRunStatusRunning {
2✔
148
                progressBar.UpdateSteps("running...  ", completedTests, totalTests)
1✔
149
        }
1✔
150
}
151

152
func (c TestRunCommand) calculateOverallProgress(status []testRunStatus) (state string, totalTests int, completedTests int) {
1✔
153
        state = TestRunStatusPackaging
1✔
154
        for _, s := range status {
2✔
155
                totalTests += s.TotalTests
1✔
156
                completedTests += s.CompletedTests
1✔
157
                if state == TestRunStatusPackaging && s.State == TestRunStatusUploading {
2✔
158
                        state = TestRunStatusUploading
1✔
159
                } else if s.State == TestRunStatusRunning {
3✔
160
                        state = TestRunStatusRunning
1✔
161
                }
1✔
162
        }
163
        return state, totalTests, completedTests
1✔
164
}
165

166
func (c TestRunCommand) execute(params testRunParams, ctx plugin.ExecutionContext, logger log.Logger, wg *sync.WaitGroup, status chan<- testRunStatus) {
1✔
167
        defer wg.Done()
1✔
168
        defer os.RemoveAll(params.Destination)
1✔
169
        packParams := newPackagePackParams(
1✔
170
                ctx.Organization,
1✔
171
                ctx.Tenant,
1✔
172
                ctx.BaseUri,
1✔
173
                ctx.Auth.Token,
1✔
174
                params.Source,
1✔
175
                params.Destination,
1✔
176
                "",
1✔
177
                true,
1✔
178
                "Tests",
1✔
179
                false,
1✔
180
                "")
1✔
181
        args := c.preparePackArguments(*packParams)
1✔
182
        exitCode, stdErr, err := params.Uipcli.ExecuteAndWait(args...)
1✔
183
        if err != nil {
1✔
NEW
184
                status <- *newTestRunStatusError(params.ExecutionId, err)
×
NEW
185
                return
×
UNCOV
186
        }
×
187
        if exitCode != 0 {
1✔
NEW
188
                status <- *newTestRunStatusError(params.ExecutionId, fmt.Errorf("Error packaging tests: %v", stdErr))
×
NEW
189
                return
×
UNCOV
190
        }
×
191

192
        nupkgPath := findLatestNupkg(params.Destination)
1✔
193
        nupkgReader := newNupkgReader(nupkgPath)
1✔
194
        nuspec, err := nupkgReader.ReadNuspec()
1✔
195
        if err != nil {
2✔
196
                status <- *newTestRunStatusError(params.ExecutionId, err)
1✔
197
                return
1✔
198
        }
1✔
199

200
        execution, err := c.runTests(params.ExecutionId, nupkgPath, nuspec.Id, nuspec.Version, params.Timeout, ctx, logger, status)
1✔
201
        if err != nil {
2✔
202
                status <- *newTestRunStatusError(params.ExecutionId, err)
1✔
203
                return
1✔
204
        }
1✔
205
        result := newTestRunResult(*execution)
1✔
206
        status <- *newTestRunStatusDone(params.ExecutionId, result.TestCasesCount, result)
1✔
207
}
208

209
func (c TestRunCommand) runTests(executionId int, nupkgPath string, processKey string, processVersion string, timeout time.Duration, ctx plugin.ExecutionContext, logger log.Logger, status chan<- testRunStatus) (*api.TestExecution, error) {
1✔
210
        status <- *newTestRunStatusUploading(executionId)
1✔
211
        baseUri := c.formatUri(ctx.BaseUri, ctx.Organization, ctx.Tenant)
1✔
212
        client := api.NewOrchestratorClient(baseUri, ctx.Auth.Token, ctx.Debug, ctx.Settings, logger)
1✔
213
        folderId, err := client.GetSharedFolderId()
1✔
214
        if err != nil {
2✔
215
                return nil, err
1✔
216
        }
1✔
217
        file := stream.NewFileStream(nupkgPath)
1✔
218
        err = client.Upload(file, nil)
1✔
219
        if err != nil {
2✔
220
                return nil, err
1✔
221
        }
1✔
222
        releaseId, err := client.CreateOrUpdateRelease(folderId, processKey, processVersion)
1✔
223
        if err != nil {
2✔
224
                return nil, err
1✔
225
        }
1✔
226
        testSetId, err := client.CreateTestSet(folderId, releaseId, processVersion)
1✔
227
        if err != nil {
2✔
228
                return nil, err
1✔
229
        }
1✔
230
        testExecutionId, err := client.ExecuteTestSet(folderId, testSetId)
1✔
231
        if err != nil {
2✔
232
                return nil, err
1✔
233
        }
1✔
234
        return client.WaitForTestExecutionToFinish(folderId, testExecutionId, timeout, func(execution api.TestExecution) {
2✔
235
                total := len(execution.TestCaseExecutions)
1✔
236
                completed := 0
1✔
237
                for _, testCase := range execution.TestCaseExecutions {
2✔
238
                        if testCase.IsCompleted() {
2✔
239
                                completed++
1✔
240
                        }
1✔
241
                }
242
                status <- *newTestRunStatusRunning(executionId, total, completed)
1✔
243
        })
244
}
245

246
func (c TestRunCommand) preparePackArguments(params packagePackParams) []string {
1✔
247
        args := []string{"package", "pack", params.Source, "--output", params.Destination}
1✔
248
        if params.PackageVersion != "" {
1✔
249
                args = append(args, "--version", params.PackageVersion)
×
250
        }
×
251
        if params.AutoVersion {
2✔
252
                args = append(args, "--autoVersion")
1✔
253
        }
1✔
254
        if params.OutputType != "" {
2✔
255
                args = append(args, "--outputType", params.OutputType)
1✔
256
        }
1✔
257
        if params.SplitOutput {
1✔
258
                args = append(args, "--splitOutput")
×
259
        }
×
260
        if params.ReleaseNotes != "" {
1✔
261
                args = append(args, "--releaseNotes", params.ReleaseNotes)
×
262
        }
×
263
        if params.AuthToken != nil && params.Organization != "" {
2✔
264
                args = append(args, "--libraryOrchestratorUrl", params.BaseUri.String())
1✔
265
                args = append(args, "--libraryOrchestratorAuthToken", params.AuthToken.Value)
1✔
266
                args = append(args, "--libraryOrchestratorAccountName", params.Organization)
1✔
267
                if params.Tenant != "" {
2✔
268
                        args = append(args, "--libraryOrchestratorTenant", params.Tenant)
1✔
269
                }
1✔
270
        }
271
        return args
1✔
272
}
273

274
func (c TestRunCommand) showPackagingProgress(progressBar *visualization.ProgressBar) chan struct{} {
1✔
275
        ticker := time.NewTicker(10 * time.Millisecond)
1✔
276
        cancel := make(chan struct{})
1✔
277
        if progressBar == nil {
1✔
NEW
278
                return cancel
×
NEW
279
        }
×
280

281
        var percent float64 = 0
1✔
282
        go func() {
2✔
283
                for {
2✔
284
                        select {
1✔
285
                        case <-ticker.C:
1✔
286
                                progressBar.UpdatePercentage("packaging...  ", percent)
1✔
287
                                percent = percent + 1
1✔
288
                                if percent > 100 {
2✔
289
                                        percent = 0
1✔
290
                                }
1✔
291
                        case <-cancel:
1✔
292
                                ticker.Stop()
1✔
293
                                return
1✔
294
                        }
295
                }
296
        }()
297
        return cancel
1✔
298
}
299

300
func (c TestRunCommand) getSources(ctx plugin.ExecutionContext) ([]string, error) {
1✔
301
        sources := c.getStringArrayParameter("source", []string{"."}, ctx.Parameters)
1✔
302
        result := []string{}
1✔
303
        for _, source := range sources {
2✔
304
                source, _ = filepath.Abs(source)
1✔
305
                fileInfo, err := os.Stat(source)
1✔
306
                if err != nil {
2✔
307
                        return []string{}, fmt.Errorf("%s not found", defaultProjectJson)
1✔
308
                }
1✔
309
                if fileInfo.IsDir() {
2✔
310
                        source = filepath.Join(source, defaultProjectJson)
1✔
311
                }
1✔
312
                result = append(result, source)
1✔
313
        }
314
        return result, nil
1✔
315
}
316

317
func (c TestRunCommand) getIntParameter(name string, defaultValue int, parameters []plugin.ExecutionParameter) int {
1✔
318
        result := defaultValue
1✔
319
        for _, p := range parameters {
2✔
320
                if p.Name == name {
2✔
321
                        if data, ok := p.Value.(int); ok {
2✔
322
                                result = data
1✔
323
                                break
1✔
324
                        }
325
                }
326
        }
327
        return result
1✔
328
}
329

330
func (c TestRunCommand) getStringArrayParameter(name string, defaultValue []string, parameters []plugin.ExecutionParameter) []string {
1✔
331
        result := defaultValue
1✔
332
        for _, p := range parameters {
2✔
333
                if p.Name == name {
2✔
334
                        if data, ok := p.Value.([]string); ok {
2✔
335
                                result = data
1✔
336
                                break
1✔
337
                        }
338
                }
339
        }
340
        return result
1✔
341
}
342

343
func (c TestRunCommand) formatUri(baseUri url.URL, org string, tenant string) string {
1✔
344
        path := baseUri.Path
1✔
345
        if baseUri.Path == "" {
2✔
346
                path = "/{organization}/{tenant}/orchestrator_"
1✔
347
        }
1✔
348
        path = strings.ReplaceAll(path, "{organization}", org)
1✔
349
        path = strings.ReplaceAll(path, "{tenant}", tenant)
1✔
350
        path = strings.TrimSuffix(path, "/")
1✔
351
        return fmt.Sprintf("%s://%s%s", baseUri.Scheme, baseUri.Host, path)
1✔
352
}
353

354
func (c TestRunCommand) randomTestRunFolderName() string {
1✔
355
        value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
1✔
356
        return "testrun-" + value.String()
1✔
357
}
1✔
358

359
func NewTestRunCommand() *TestRunCommand {
1✔
360
        return &TestRunCommand{process.NewExecProcess()}
1✔
361
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc