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

UiPath / uipathcli / 14428345143

13 Apr 2025 09:49AM UTC coverage: 90.047% (-0.4%) from 90.463%
14428345143

push

github

web-flow
Merge pull request #170 from UiPath/feature/test-run-parallel

Add support to run tests from multiple sources in parallel

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

2 existing lines in 1 file now uncovered.

6188 of 6872 relevant lines covered (90.05%)

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

© 2025 Coveralls, Inc