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

UiPath / uipathcli / 14446060156

14 Apr 2025 12:49PM UTC coverage: 89.92% (-0.1%) from 90.047%
14446060156

push

github

web-flow
Merge pull request #171 from UiPath/feature/test-run-junit-robotlogs

Extend test run command to support JUnit and retrieve robot logs

397 of 445 new or added lines in 13 files covered. (89.21%)

1 existing line in 1 file now uncovered.

6503 of 7232 relevant lines covered (89.92%)

1.01 hits per line

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

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

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

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

30
var resultsOutputAllowedValues = []string{"uipath", "junit"}
31

32
// The TestRunCommand packs a project as a test package,
33
// uploads it to the connected Orchestrator instances
34
// and runs the tests.
35
type TestRunCommand struct {
36
        Exec process.ExecProcess
37
}
38

39
func (c TestRunCommand) Command() plugin.Command {
1✔
40
        return *plugin.NewCommand("studio").
1✔
41
                WithCategory("test", "Test", "Tests your UiPath studio packages").
1✔
42
                WithOperation("run", "Run Tests", "Tests a given package").
1✔
43
                WithParameter("source", plugin.ParameterTypeStringArray, "Path to one or more project.json files or folders containing project.json files (default: .)", false).
1✔
44
                WithParameter("timeout", plugin.ParameterTypeInteger, "Time to wait in seconds for tests to finish (default: 3600)", false).
1✔
45
                WithParameter("results-output", plugin.ParameterTypeString, "Output type for the test results report (default: uipath)"+c.formatAllowedValues(resultsOutputAllowedValues), false).
1✔
46
                WithParameter("attach-robot-logs", plugin.ParameterTypeBoolean, "Attaches Robot Logs for each testcases along with Test Report.", false)
1✔
47
}
1✔
48

49
func (c TestRunCommand) Execute(ctx plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
50
        sources, err := c.getSources(ctx)
1✔
51
        if err != nil {
2✔
52
                return err
1✔
53
        }
1✔
54
        timeout := time.Duration(c.getIntParameter("timeout", 3600, ctx.Parameters)) * time.Second
1✔
55
        resultsOutput := c.getParameter("results-output", "uipath", ctx.Parameters)
1✔
56
        if resultsOutput != "" && !slices.Contains(resultsOutputAllowedValues, resultsOutput) {
1✔
NEW
57
                return fmt.Errorf("Invalid output type '%s', allowed values: %s", resultsOutput, strings.Join(resultsOutputAllowedValues, ", "))
×
NEW
58
        }
×
59
        attachRobotLogs := c.getBoolParameter("attach-robot-logs", false, ctx.Parameters)
1✔
60

1✔
61
        params, err := c.prepareExecution(sources, timeout, attachRobotLogs, logger)
1✔
62
        if err != nil {
1✔
63
                return err
×
64
        }
×
65
        result, err := c.executeAll(params, ctx, logger)
1✔
66
        if err != nil {
2✔
67
                return err
1✔
68
        }
1✔
69
        return c.writeOutput(ctx, result, resultsOutput, writer)
1✔
70
}
71

72
func (c TestRunCommand) writeOutput(ctx plugin.ExecutionContext, results []testRunStatus, resultsOutput string, writer output.OutputWriter) error {
1✔
73
        var data []byte
1✔
74
        var err error
1✔
75
        if resultsOutput == "uipath" {
2✔
76
                converter := newUiPathReportConverter()
1✔
77
                report := converter.Convert(results)
1✔
78
                data, err = json.Marshal(report)
1✔
79
                if err != nil {
1✔
NEW
80
                        return fmt.Errorf("run command failed: %v", err)
×
NEW
81
                }
×
82
        } else {
1✔
83
                baseUri := c.formatUri(ctx.BaseUri, ctx.Organization, ctx.Tenant)
1✔
84
                converter := newJUnitReportConverter(baseUri)
1✔
85
                report := converter.Convert(results)
1✔
86
                data, err = xml.MarshalIndent(report, "", "  ")
1✔
87
                if err != nil {
1✔
NEW
88
                        return fmt.Errorf("run command failed: %v", err)
×
NEW
89
                }
×
90
        }
91
        return writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(data)))
1✔
92
}
93

94
func (c TestRunCommand) prepareExecution(sources []string, timeout time.Duration, attachRobotLogs bool, logger log.Logger) ([]testRunParams, error) {
1✔
95
        tmp, err := directories.Temp()
1✔
96
        if err != nil {
1✔
97
                return nil, err
×
98
        }
×
99

100
        params := []testRunParams{}
1✔
101
        for i, source := range sources {
2✔
102
                projectReader := newStudioProjectReader(source)
1✔
103
                project, err := projectReader.ReadMetadata()
1✔
104
                if err != nil {
1✔
105
                        return nil, err
×
106
                }
×
107
                supported, err := project.TargetFramework.IsSupported()
1✔
108
                if !supported {
1✔
109
                        return nil, err
×
110
                }
×
111

112
                executionLogger := logger
1✔
113
                if len(sources) > 1 {
1✔
114
                        executionLogger = NewMultiLogger(logger, "["+strconv.Itoa(i+1)+"] ")
×
115
                }
×
116
                uipcli := newUipcli(c.Exec, executionLogger)
1✔
117
                err = uipcli.Initialize(project.TargetFramework)
1✔
118
                if err != nil {
1✔
119
                        return nil, err
×
120
                }
×
121
                destination := filepath.Join(tmp, c.randomTestRunFolderName())
1✔
122
                params = append(params, *newTestRunParams(i, uipcli, executionLogger, source, destination, timeout, attachRobotLogs))
1✔
123
        }
124
        return params, nil
1✔
125
}
126

127
func (c TestRunCommand) executeAll(params []testRunParams, ctx plugin.ExecutionContext, logger log.Logger) ([]testRunStatus, error) {
1✔
128
        statusChannel := make(chan testRunStatus)
1✔
129
        var wg sync.WaitGroup
1✔
130
        for _, p := range params {
2✔
131
                wg.Add(1)
1✔
132
                go c.execute(p, ctx, p.Logger, &wg, statusChannel)
1✔
133
        }
1✔
134

135
        go func() {
2✔
136
                wg.Wait()
1✔
137
                close(statusChannel)
1✔
138
        }()
1✔
139

140
        var progressBar *visualization.ProgressBar
1✔
141
        if !ctx.Debug {
2✔
142
                progressBar = visualization.NewProgressBar(logger)
1✔
143
                defer progressBar.Remove()
1✔
144
        }
1✔
145
        once := sync.Once{}
1✔
146
        progress := c.showPackagingProgress(progressBar)
1✔
147
        defer once.Do(func() { close(progress) })
1✔
148

149
        status := make([]testRunStatus, len(params))
1✔
150
        for s := range statusChannel {
2✔
151
                once.Do(func() { close(progress) })
2✔
152
                status[s.ExecutionId] = s
1✔
153
                c.updateProgressBar(progressBar, status)
1✔
154
        }
155

156
        results := []testRunStatus{}
1✔
157
        for _, s := range status {
2✔
158
                if s.Err != nil {
2✔
159
                        return nil, s.Err
1✔
160
                }
1✔
161
                results = append(results, s)
1✔
162
        }
163
        return results, nil
1✔
164
}
165

166
func (c TestRunCommand) updateProgressBar(progressBar *visualization.ProgressBar, status []testRunStatus) {
1✔
167
        if progressBar == nil {
1✔
168
                return
×
169
        }
×
170
        state, totalTests, completedTests := c.calculateOverallProgress(status)
1✔
171
        if state == TestRunStatusUploading {
2✔
172
                progressBar.UpdatePercentage("uploading...", 0)
1✔
173
        } else if state == TestRunStatusRunning && totalTests == 0 && completedTests == 0 {
2✔
174
                progressBar.UpdatePercentage("running...  ", 0)
×
175
        } else if state == TestRunStatusRunning {
2✔
176
                progressBar.UpdateSteps("running...  ", completedTests, totalTests)
1✔
177
        }
1✔
178
}
179

180
func (c TestRunCommand) calculateOverallProgress(status []testRunStatus) (state string, totalTests int, completedTests int) {
1✔
181
        state = TestRunStatusPackaging
1✔
182
        for _, s := range status {
2✔
183
                totalTests += s.TotalTests
1✔
184
                completedTests += s.CompletedTests
1✔
185
                if state == TestRunStatusPackaging && s.State == TestRunStatusUploading {
2✔
186
                        state = TestRunStatusUploading
1✔
187
                } else if s.State == TestRunStatusRunning {
3✔
188
                        state = TestRunStatusRunning
1✔
189
                }
1✔
190
        }
191
        return state, totalTests, completedTests
1✔
192
}
193

194
func (c TestRunCommand) execute(params testRunParams, ctx plugin.ExecutionContext, logger log.Logger, wg *sync.WaitGroup, status chan<- testRunStatus) {
1✔
195
        defer wg.Done()
1✔
196
        defer os.RemoveAll(params.Destination)
1✔
197
        packParams := newPackagePackParams(
1✔
198
                ctx.Organization,
1✔
199
                ctx.Tenant,
1✔
200
                ctx.BaseUri,
1✔
201
                ctx.Auth.Token,
1✔
202
                params.Source,
1✔
203
                params.Destination,
1✔
204
                "",
1✔
205
                true,
1✔
206
                "Tests",
1✔
207
                false,
1✔
208
                "")
1✔
209
        args := c.preparePackArguments(*packParams)
1✔
210
        exitCode, stdErr, err := params.Uipcli.ExecuteAndWait(args...)
1✔
211
        if err != nil {
1✔
212
                status <- *newTestRunStatusError(params.ExecutionId, err)
×
213
                return
×
214
        }
×
215
        if exitCode != 0 {
1✔
216
                status <- *newTestRunStatusError(params.ExecutionId, fmt.Errorf("Error packaging tests: %v", stdErr))
×
217
                return
×
218
        }
×
219

220
        nupkgPath := findLatestNupkg(params.Destination)
1✔
221
        nupkgReader := newNupkgReader(nupkgPath)
1✔
222
        nuspec, err := nupkgReader.ReadNuspec()
1✔
223
        if err != nil {
2✔
224
                status <- *newTestRunStatusError(params.ExecutionId, err)
1✔
225
                return
1✔
226
        }
1✔
227

228
        folderId, testSet, execution, err := c.runTests(params.ExecutionId, nupkgPath, nuspec.Id, nuspec.Version, params.Timeout, params.AttachRobotLogs, ctx, logger, status)
1✔
229
        if err != nil {
2✔
230
                status <- *newTestRunStatusError(params.ExecutionId, err)
1✔
231
                return
1✔
232
        }
1✔
233
        status <- *newTestRunStatusDone(params.ExecutionId, folderId, len(execution.TestCaseExecutions), testSet, execution)
1✔
234
}
235

236
func (c TestRunCommand) runTests(executionId int, nupkgPath string, processKey string, processVersion string, timeout time.Duration, attachRobotLogs bool, ctx plugin.ExecutionContext, logger log.Logger, status chan<- testRunStatus) (int, *api.TestSet, *api.TestExecution, error) {
1✔
237
        status <- *newTestRunStatusUploading(executionId)
1✔
238
        baseUri := c.formatUri(ctx.BaseUri, ctx.Organization, ctx.Tenant)
1✔
239
        client := api.NewOrchestratorClient(baseUri, ctx.Auth.Token, ctx.Debug, ctx.Settings, logger)
1✔
240
        folderId, err := client.GetSharedFolderId()
1✔
241
        if err != nil {
2✔
242
                return -1, nil, nil, err
1✔
243
        }
1✔
244
        file := stream.NewFileStream(nupkgPath)
1✔
245
        err = client.Upload(file, nil)
1✔
246
        if err != nil {
2✔
247
                return -1, nil, nil, err
1✔
248
        }
1✔
249
        releaseId, err := client.CreateOrUpdateRelease(folderId, processKey, processVersion)
1✔
250
        if err != nil {
2✔
251
                return -1, nil, nil, err
1✔
252
        }
1✔
253
        testSetId, err := client.CreateTestSet(folderId, releaseId, processVersion)
1✔
254
        if err != nil {
2✔
255
                return -1, nil, nil, err
1✔
256
        }
1✔
257
        testExecutionId, err := client.ExecuteTestSet(folderId, testSetId)
1✔
258
        if err != nil {
2✔
259
                return -1, nil, nil, err
1✔
260
        }
1✔
261
        testSet, err := client.GetTestSet(folderId, testSetId)
1✔
262
        if err != nil {
1✔
NEW
263
                return -1, nil, nil, err
×
UNCOV
264
        }
×
265
        testExecution, err := client.WaitForTestExecutionToFinish(folderId, testExecutionId, timeout, func(execution api.TestExecution) {
2✔
266
                total := len(execution.TestCaseExecutions)
1✔
267
                completed := 0
1✔
268
                for _, testCase := range execution.TestCaseExecutions {
2✔
269
                        if testCase.IsCompleted() {
2✔
270
                                completed++
1✔
271
                        }
1✔
272
                }
273
                status <- *newTestRunStatusRunning(executionId, folderId, total, completed)
1✔
274
        })
275

276
        if testExecution != nil && attachRobotLogs {
2✔
277
                for idx, testCase := range testExecution.TestCaseExecutions {
2✔
278
                        robotLogs, err := client.GetRobotLogs(folderId, testCase.JobKey)
1✔
279
                        if err != nil {
1✔
NEW
280
                                return -1, nil, nil, err
×
NEW
281
                        }
×
282
                        testExecution.TestCaseExecutions[idx].SetRobotLogs(robotLogs)
1✔
283
                }
284
        }
285
        return folderId, testSet, testExecution, err
1✔
286
}
287

288
func (c TestRunCommand) preparePackArguments(params packagePackParams) []string {
1✔
289
        args := []string{"package", "pack", params.Source, "--output", params.Destination}
1✔
290
        if params.PackageVersion != "" {
1✔
291
                args = append(args, "--version", params.PackageVersion)
×
292
        }
×
293
        if params.AutoVersion {
2✔
294
                args = append(args, "--autoVersion")
1✔
295
        }
1✔
296
        if params.OutputType != "" {
2✔
297
                args = append(args, "--outputType", params.OutputType)
1✔
298
        }
1✔
299
        if params.SplitOutput {
1✔
300
                args = append(args, "--splitOutput")
×
301
        }
×
302
        if params.ReleaseNotes != "" {
1✔
303
                args = append(args, "--releaseNotes", params.ReleaseNotes)
×
304
        }
×
305
        if params.AuthToken != nil && params.Organization != "" {
2✔
306
                args = append(args, "--libraryOrchestratorUrl", params.BaseUri.String())
1✔
307
                args = append(args, "--libraryOrchestratorAuthToken", params.AuthToken.Value)
1✔
308
                args = append(args, "--libraryOrchestratorAccountName", params.Organization)
1✔
309
                if params.Tenant != "" {
2✔
310
                        args = append(args, "--libraryOrchestratorTenant", params.Tenant)
1✔
311
                }
1✔
312
        }
313
        return args
1✔
314
}
315

316
func (c TestRunCommand) showPackagingProgress(progressBar *visualization.ProgressBar) chan struct{} {
1✔
317
        ticker := time.NewTicker(10 * time.Millisecond)
1✔
318
        cancel := make(chan struct{})
1✔
319
        if progressBar == nil {
1✔
320
                return cancel
×
321
        }
×
322

323
        var percent float64 = 0
1✔
324
        go func() {
2✔
325
                for {
2✔
326
                        select {
1✔
327
                        case <-ticker.C:
1✔
328
                                progressBar.UpdatePercentage("packaging...  ", percent)
1✔
329
                                percent = percent + 1
1✔
330
                                if percent > 100 {
2✔
331
                                        percent = 0
1✔
332
                                }
1✔
333
                        case <-cancel:
1✔
334
                                ticker.Stop()
1✔
335
                                return
1✔
336
                        }
337
                }
338
        }()
339
        return cancel
1✔
340
}
341

342
func (c TestRunCommand) getSources(ctx plugin.ExecutionContext) ([]string, error) {
1✔
343
        sources := c.getStringArrayParameter("source", []string{"."}, ctx.Parameters)
1✔
344
        result := []string{}
1✔
345
        for _, source := range sources {
2✔
346
                source, _ = filepath.Abs(source)
1✔
347
                fileInfo, err := os.Stat(source)
1✔
348
                if err != nil {
2✔
349
                        return []string{}, fmt.Errorf("%s not found", defaultProjectJson)
1✔
350
                }
1✔
351
                if fileInfo.IsDir() {
2✔
352
                        source = filepath.Join(source, defaultProjectJson)
1✔
353
                }
1✔
354
                result = append(result, source)
1✔
355
        }
356
        return result, nil
1✔
357
}
358

359
func (c TestRunCommand) getIntParameter(name string, defaultValue int, parameters []plugin.ExecutionParameter) int {
1✔
360
        result := defaultValue
1✔
361
        for _, p := range parameters {
2✔
362
                if p.Name == name {
2✔
363
                        if data, ok := p.Value.(int); ok {
2✔
364
                                result = data
1✔
365
                                break
1✔
366
                        }
367
                }
368
        }
369
        return result
1✔
370
}
371

372
func (c TestRunCommand) getBoolParameter(name string, defaultValue bool, parameters []plugin.ExecutionParameter) bool {
1✔
373
        result := defaultValue
1✔
374
        for _, p := range parameters {
2✔
375
                if p.Name == name {
2✔
376
                        if data, ok := p.Value.(bool); ok {
2✔
377
                                result = data
1✔
378
                                break
1✔
379
                        }
380
                }
381
        }
382
        return result
1✔
383
}
384

385
func (c TestRunCommand) getStringArrayParameter(name string, defaultValue []string, parameters []plugin.ExecutionParameter) []string {
1✔
386
        result := defaultValue
1✔
387
        for _, p := range parameters {
2✔
388
                if p.Name == name {
2✔
389
                        if data, ok := p.Value.([]string); ok {
2✔
390
                                result = data
1✔
391
                                break
1✔
392
                        }
393
                }
394
        }
395
        return result
1✔
396
}
397

398
func (c TestRunCommand) getParameter(name string, defaultValue string, parameters []plugin.ExecutionParameter) string {
1✔
399
        result := defaultValue
1✔
400
        for _, p := range parameters {
2✔
401
                if p.Name == name {
2✔
402
                        if data, ok := p.Value.(string); ok {
2✔
403
                                result = data
1✔
404
                                break
1✔
405
                        }
406
                }
407
        }
408
        return result
1✔
409
}
410

411
func (c TestRunCommand) formatUri(baseUri url.URL, org string, tenant string) string {
1✔
412
        path := baseUri.Path
1✔
413
        if baseUri.Path == "" {
2✔
414
                path = "/{organization}/{tenant}/orchestrator_"
1✔
415
        }
1✔
416
        path = strings.ReplaceAll(path, "{organization}", org)
1✔
417
        path = strings.ReplaceAll(path, "{tenant}", tenant)
1✔
418
        path = strings.TrimSuffix(path, "/")
1✔
419
        return fmt.Sprintf("%s://%s%s", baseUri.Scheme, baseUri.Host, path)
1✔
420
}
421

422
func (c TestRunCommand) randomTestRunFolderName() string {
1✔
423
        value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
1✔
424
        return "testrun-" + value.String()
1✔
425
}
1✔
426

427
func (c TestRunCommand) formatAllowedValues(allowed []string) string {
1✔
428
        return "\n\nAllowed Values:\n- " + strings.Join(allowed, "\n- ")
1✔
429
}
1✔
430

431
func NewTestRunCommand() *TestRunCommand {
1✔
432
        return &TestRunCommand{process.NewExecProcess()}
1✔
433
}
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