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

UiPath / uipathcli / 14420384542

12 Apr 2025 02:08PM UTC coverage: 90.547% (+0.08%) from 90.463%
14420384542

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.

128 of 139 new or added lines in 6 files covered. (92.09%)

2 existing lines in 1 file now uncovered.

6178 of 6823 relevant lines covered (90.55%)

1.02 hits per line

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

90.87
/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
        "strings"
14
        "sync"
15
        "time"
16

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

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

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

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

53
        json, err := json.Marshal(result)
1✔
54
        if err != nil {
1✔
55
                return fmt.Errorf("pack command failed: %v", err)
×
56
        }
×
57
        return writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
1✔
58
}
59

60
func (c TestRunCommand) executeAll(sources []string, timeout time.Duration, ctx plugin.ExecutionContext, logger log.Logger) (*testRunReport, error) {
1✔
61
        tmp, err := directories.Temp()
1✔
62
        if err != nil {
1✔
63
                return nil, err
×
64
        }
×
65

66
        params := []testRunParams{}
1✔
67
        for i, source := range sources {
2✔
68
                projectReader := newStudioProjectReader(source)
1✔
69
                project, err := projectReader.ReadMetadata()
1✔
70
                if err != nil {
1✔
NEW
71
                        return nil, err
×
NEW
72
                }
×
73
                supported, err := project.TargetFramework.IsSupported()
1✔
74
                if !supported {
1✔
NEW
75
                        return nil, err
×
NEW
76
                }
×
77

78
                uipcli := newUipcli(c.Exec, logger)
1✔
79
                err = uipcli.Initialize(project.TargetFramework)
1✔
80
                if err != nil {
1✔
NEW
81
                        return nil, err
×
NEW
82
                }
×
83
                destination := filepath.Join(tmp, c.randomTestRunFolderName())
1✔
84
                params = append(params, *newTestRunParams(i, uipcli, source, destination, timeout))
1✔
85
        }
86

87
        statusChannel := make(chan testRunStatus)
1✔
88
        var wg sync.WaitGroup
1✔
89
        for _, p := range params {
2✔
90
                wg.Add(1)
1✔
91
                go c.execute(p, ctx, logger, &wg, statusChannel)
1✔
92
        }
1✔
93

94
        go func() {
2✔
95
                wg.Wait()
1✔
96
                close(statusChannel)
1✔
97
        }()
1✔
98

99
        progressBar := visualization.NewProgressBar(logger)
1✔
100
        defer progressBar.Remove()
1✔
101
        progress := c.showPackagingProgress(progressBar)
1✔
102
        once := sync.Once{}
1✔
103
        defer once.Do(func() { close(progress) })
1✔
104

105
        status := make([]testRunStatus, len(params))
1✔
106
        for s := range statusChannel {
2✔
107
                once.Do(func() { close(progress) })
2✔
108
                status[s.ExecutionId] = s
1✔
109
                c.updateProgressBar(progressBar, status)
1✔
110
        }
111

112
        results := []testRunResult{}
1✔
113
        for _, s := range status {
2✔
114
                if s.Err != nil {
2✔
115
                        return nil, s.Err
1✔
116
                }
1✔
117
                results = append(results, *s.Result)
1✔
118
        }
119
        return newTestRunReport(results), nil
1✔
120
}
121

122
func (c TestRunCommand) updateProgressBar(progressBar *visualization.ProgressBar, status []testRunStatus) {
1✔
123
        state, totalTests, completedTests := c.calculateOverallProgress(status)
1✔
124
        if state == TestRunStatusUploading {
2✔
125
                progressBar.UpdatePercentage("uploading...", 0)
1✔
126
        } else if state == TestRunStatusRunning && totalTests == 0 && completedTests == 0 {
2✔
NEW
127
                progressBar.UpdatePercentage("running...  ", 0)
×
128
        } else if state == TestRunStatusRunning {
2✔
129
                progressBar.UpdateSteps("running...  ", completedTests, totalTests)
1✔
130
        }
1✔
131
}
132

133
func (c TestRunCommand) calculateOverallProgress(status []testRunStatus) (state string, totalTests int, completedTests int) {
1✔
134
        state = TestRunStatusPackaging
1✔
135
        for _, s := range status {
2✔
136
                totalTests += s.TotalTests
1✔
137
                completedTests += s.CompletedTests
1✔
138
                if state == TestRunStatusPackaging && s.State == TestRunStatusUploading {
2✔
139
                        state = TestRunStatusUploading
1✔
140
                } else if s.State == TestRunStatusRunning {
3✔
141
                        state = TestRunStatusRunning
1✔
142
                }
1✔
143
        }
144
        return state, totalTests, completedTests
1✔
145
}
146

147
func (c TestRunCommand) execute(params testRunParams, ctx plugin.ExecutionContext, logger log.Logger, wg *sync.WaitGroup, status chan<- testRunStatus) {
1✔
148
        defer wg.Done()
1✔
149
        defer os.RemoveAll(params.Destination)
1✔
150
        packParams := newPackagePackParams(
1✔
151
                ctx.Organization,
1✔
152
                ctx.Tenant,
1✔
153
                ctx.BaseUri,
1✔
154
                ctx.Auth.Token,
1✔
155
                params.Source,
1✔
156
                params.Destination,
1✔
157
                "",
1✔
158
                true,
1✔
159
                "Tests",
1✔
160
                false,
1✔
161
                "")
1✔
162
        args := c.preparePackArguments(*packParams)
1✔
163
        exitCode, stdErr, err := params.Uipcli.ExecuteAndWait(args...)
1✔
164
        if err != nil {
1✔
NEW
165
                status <- *newTestRunStatusError(params.ExecutionId, err)
×
NEW
166
                return
×
UNCOV
167
        }
×
168
        if exitCode != 0 {
1✔
NEW
169
                status <- *newTestRunStatusError(params.ExecutionId, fmt.Errorf("Error packaging tests: %v", stdErr))
×
NEW
170
                return
×
UNCOV
171
        }
×
172

173
        nupkgPath := findLatestNupkg(params.Destination)
1✔
174
        nupkgReader := newNupkgReader(nupkgPath)
1✔
175
        nuspec, err := nupkgReader.ReadNuspec()
1✔
176
        if err != nil {
2✔
177
                status <- *newTestRunStatusError(params.ExecutionId, err)
1✔
178
                return
1✔
179
        }
1✔
180

181
        execution, err := c.runTests(params.ExecutionId, nupkgPath, nuspec.Id, nuspec.Version, params.Timeout, ctx, logger, status)
1✔
182
        if err != nil {
2✔
183
                status <- *newTestRunStatusError(params.ExecutionId, err)
1✔
184
                return
1✔
185
        }
1✔
186
        result := newTestRunResult(*execution)
1✔
187
        status <- *newTestRunStatusDone(params.ExecutionId, result.TestCasesCount, result)
1✔
188
}
189

190
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✔
191
        status <- *newTestRunStatusUploading(executionId)
1✔
192
        baseUri := c.formatUri(ctx.BaseUri, ctx.Organization, ctx.Tenant)
1✔
193
        client := api.NewOrchestratorClient(baseUri, ctx.Auth.Token, ctx.Debug, ctx.Settings, logger)
1✔
194
        folderId, err := client.GetSharedFolderId()
1✔
195
        if err != nil {
2✔
196
                return nil, err
1✔
197
        }
1✔
198
        file := stream.NewFileStream(nupkgPath)
1✔
199
        err = client.Upload(file, nil)
1✔
200
        if err != nil {
2✔
201
                return nil, err
1✔
202
        }
1✔
203
        releaseId, err := client.CreateOrUpdateRelease(folderId, processKey, processVersion)
1✔
204
        if err != nil {
2✔
205
                return nil, err
1✔
206
        }
1✔
207
        testSetId, err := client.CreateTestSet(folderId, releaseId, processVersion)
1✔
208
        if err != nil {
2✔
209
                return nil, err
1✔
210
        }
1✔
211
        testExecutionId, err := client.ExecuteTestSet(folderId, testSetId)
1✔
212
        if err != nil {
2✔
213
                return nil, err
1✔
214
        }
1✔
215
        return client.WaitForTestExecutionToFinish(folderId, testExecutionId, timeout, func(execution api.TestExecution) {
2✔
216
                total := len(execution.TestCaseExecutions)
1✔
217
                completed := 0
1✔
218
                for _, testCase := range execution.TestCaseExecutions {
2✔
219
                        if testCase.IsCompleted() {
2✔
220
                                completed++
1✔
221
                        }
1✔
222
                }
223
                status <- *newTestRunStatusRunning(executionId, total, completed)
1✔
224
        })
225
}
226

227
func (c TestRunCommand) preparePackArguments(params packagePackParams) []string {
1✔
228
        args := []string{"package", "pack", params.Source, "--output", params.Destination}
1✔
229
        if params.PackageVersion != "" {
1✔
230
                args = append(args, "--version", params.PackageVersion)
×
231
        }
×
232
        if params.AutoVersion {
2✔
233
                args = append(args, "--autoVersion")
1✔
234
        }
1✔
235
        if params.OutputType != "" {
2✔
236
                args = append(args, "--outputType", params.OutputType)
1✔
237
        }
1✔
238
        if params.SplitOutput {
1✔
239
                args = append(args, "--splitOutput")
×
240
        }
×
241
        if params.ReleaseNotes != "" {
1✔
242
                args = append(args, "--releaseNotes", params.ReleaseNotes)
×
243
        }
×
244
        if params.AuthToken != nil && params.Organization != "" {
2✔
245
                args = append(args, "--libraryOrchestratorUrl", params.BaseUri.String())
1✔
246
                args = append(args, "--libraryOrchestratorAuthToken", params.AuthToken.Value)
1✔
247
                args = append(args, "--libraryOrchestratorAccountName", params.Organization)
1✔
248
                if params.Tenant != "" {
2✔
249
                        args = append(args, "--libraryOrchestratorTenant", params.Tenant)
1✔
250
                }
1✔
251
        }
252
        return args
1✔
253
}
254

255
func (c TestRunCommand) showPackagingProgress(progressBar *visualization.ProgressBar) chan struct{} {
1✔
256
        ticker := time.NewTicker(10 * time.Millisecond)
1✔
257
        cancel := make(chan struct{})
1✔
258
        var percent float64 = 0
1✔
259
        go func() {
2✔
260
                for {
2✔
261
                        select {
1✔
262
                        case <-ticker.C:
1✔
263
                                progressBar.UpdatePercentage("packaging...  ", percent)
1✔
264
                                percent = percent + 1
1✔
265
                                if percent > 100 {
2✔
266
                                        percent = 0
1✔
267
                                }
1✔
268
                        case <-cancel:
1✔
269
                                ticker.Stop()
1✔
270
                                return
1✔
271
                        }
272
                }
273
        }()
274
        return cancel
1✔
275
}
276

277
func (c TestRunCommand) getSources(ctx plugin.ExecutionContext) ([]string, error) {
1✔
278
        sources := c.getStringArrayParameter("source", []string{"."}, ctx.Parameters)
1✔
279
        result := []string{}
1✔
280
        for _, source := range sources {
2✔
281
                source, _ = filepath.Abs(source)
1✔
282
                fileInfo, err := os.Stat(source)
1✔
283
                if err != nil {
2✔
284
                        return []string{}, fmt.Errorf("%s not found", defaultProjectJson)
1✔
285
                }
1✔
286
                if fileInfo.IsDir() {
2✔
287
                        source = filepath.Join(source, defaultProjectJson)
1✔
288
                }
1✔
289
                result = append(result, source)
1✔
290
        }
291
        return result, nil
1✔
292
}
293

294
func (c TestRunCommand) getIntParameter(name string, defaultValue int, parameters []plugin.ExecutionParameter) int {
1✔
295
        result := defaultValue
1✔
296
        for _, p := range parameters {
2✔
297
                if p.Name == name {
2✔
298
                        if data, ok := p.Value.(int); ok {
2✔
299
                                result = data
1✔
300
                                break
1✔
301
                        }
302
                }
303
        }
304
        return result
1✔
305
}
306

307
func (c TestRunCommand) getStringArrayParameter(name string, defaultValue []string, parameters []plugin.ExecutionParameter) []string {
1✔
308
        result := defaultValue
1✔
309
        for _, p := range parameters {
2✔
310
                if p.Name == name {
2✔
311
                        if data, ok := p.Value.([]string); ok {
2✔
312
                                result = data
1✔
313
                                break
1✔
314
                        }
315
                }
316
        }
317
        return result
1✔
318
}
319

320
func (c TestRunCommand) formatUri(baseUri url.URL, org string, tenant string) string {
1✔
321
        path := baseUri.Path
1✔
322
        if baseUri.Path == "" {
2✔
323
                path = "/{organization}/{tenant}/orchestrator_"
1✔
324
        }
1✔
325
        path = strings.ReplaceAll(path, "{organization}", org)
1✔
326
        path = strings.ReplaceAll(path, "{tenant}", tenant)
1✔
327
        path = strings.TrimSuffix(path, "/")
1✔
328
        return fmt.Sprintf("%s://%s%s", baseUri.Scheme, baseUri.Host, path)
1✔
329
}
330

331
func (c TestRunCommand) randomTestRunFolderName() string {
1✔
332
        value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
1✔
333
        return "testrun-" + value.String()
1✔
334
}
1✔
335

336
func NewTestRunCommand() *TestRunCommand {
1✔
337
        return &TestRunCommand{process.NewExecProcess()}
1✔
338
}
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