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

UiPath / uipathcli / 14420832619

12 Apr 2025 03:11PM UTC coverage: 90.209% (-0.3%) from 90.463%
14420832619

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.

153 of 192 new or added lines in 8 files covered. (79.69%)

2 existing lines in 1 file now uncovered.

6201 of 6874 relevant lines covered (90.21%)

1.01 hits per line

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

88.69
/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
        isParallelRun := len(sources) > 1
1✔
73
        params := []testRunParams{}
1✔
74
        for i, source := range sources {
2✔
75
                projectReader := newStudioProjectReader(source)
1✔
76
                project, err := projectReader.ReadMetadata()
1✔
77
                if err != nil {
1✔
NEW
78
                        return nil, err
×
NEW
79
                }
×
80
                supported, err := project.TargetFramework.IsSupported()
1✔
81
                if !supported {
1✔
NEW
82
                        return nil, err
×
NEW
83
                }
×
84

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

361
func NewTestRunCommand() *TestRunCommand {
1✔
362
        return &TestRunCommand{process.NewExecProcess()}
1✔
363
}
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