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

HicServices / RDMP / 9345538130

03 Jun 2024 06:46AM UTC coverage: 56.914%. Remained the same
9345538130

push

github

web-flow
Efficiency enhancements (#1829)

* More efficient checking for duplicates in lists

* GetValueOrDefault instead of manual fallback

* Update CommentStore.cs

Eliminate double-searching of CommentStore

* Eliminate more double-searches

* Update ExtractionConfiguration.cs

Clearer variable type, remove redundant casts

* Update ExecuteCommandAddPipelineComponent.cs

Make callbacks static for simplicity

* Update ColumnInfoANOPlan.cs

Join declaration and initialisation

* More GetValueOrDefault

* Simplify searching logic - Where(x).First() -> First(x) to avoid repeating copying

* Update CatalogueChildProvider.cs

Simplify list generation logic

* Update DleRunner.cs

Remove redundant counting of list size

* Null-coalesce, usings

* Lambdas

* Update YesNoYesToAllDialog.cs

Avoid thread-thrashing if yes or no to all has been clicked

* Method groups, static lambdas

* Avoid multiple lookups when iterating arrays

* Simplify null checks

* String interpolation, another loop deref

* Update CohortIdentificationTaskExecution.cs

Fix duplicate Dispose calls

* Update RemoteAttacher.cs

Make fake conditional value a constant

* Fix multiple enumerations

A

* Type check and cast combination

* Update RemoteAttacherTests.cs

String interpolation, object initialisers

* Turn some private fields to local variables where applicable

* No-op string interpolations

* Remove some redundant syntax

10817 of 20482 branches covered (52.81%)

Branch coverage included in aggregate %.

152 of 231 new or added lines in 53 files covered. (65.8%)

3 existing lines in 2 files now uncovered.

30827 of 52688 relevant lines covered (58.51%)

7412.54 hits per line

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

6.36
/Rdmp.Core/CommandLine/Interactive/ConsoleInputManager.cs
1
// Copyright (c) The University of Dundee 2018-2019
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using System;
8
using System.Collections.Generic;
9
using System.Data;
10
using System.Diagnostics;
11
using System.IO;
12
using System.Linq;
13
using System.Text;
14
using System.Threading;
15
using System.Threading.Tasks;
16
using FAnsi.Discovery;
17
using Rdmp.Core.CommandExecution;
18
using Rdmp.Core.CommandLine.Interactive.Picking;
19
using Rdmp.Core.Curation.Data.Aggregation;
20
using Rdmp.Core.Curation.Data.DataLoad;
21
using Rdmp.Core.DataExport.DataExtraction;
22
using Rdmp.Core.DataViewing;
23
using Rdmp.Core.MapsDirectlyToDatabaseTable;
24
using Rdmp.Core.Repositories;
25
using Rdmp.Core.ReusableLibraryCode;
26
using Rdmp.Core.ReusableLibraryCode.Checks;
27
using Rdmp.Core.ReusableLibraryCode.DataAccess;
28
using Spectre.Console;
29

30
namespace Rdmp.Core.CommandLine.Interactive;
31

32
/// <summary>
33
/// Implementation of <see cref="IBasicActivateItems"/> that handles object selection and message notification but is <see cref="IsInteractive"/>=false and throws <see cref="InputDisallowedException"/> on any attempt to illicit user feedback
34
/// </summary>
35
public class ConsoleInputManager : BasicActivateItems
36
{
37
    /// <inheritdoc/>
38
    public override bool IsInteractive => !DisallowInput;
17,848✔
39

40
    /// <summary>
41
    /// Set to true to throw on any blocking input methods (e.g. <see cref="TypeText"/>)
42
    /// </summary>
43
    public bool DisallowInput { get; set; }
18,116✔
44

45
    /// <summary>
46
    /// Creates a new instance connected to the provided RDMP platform databases
47
    /// </summary>
48
    /// <param name="repositoryLocator">The databases to connect to</param>
49
    /// <param name="globalErrorCheckNotifier">The global error provider for non fatal issues</param>
50
    public ConsoleInputManager(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
51
        ICheckNotifier globalErrorCheckNotifier) : base(repositoryLocator, globalErrorCheckNotifier)
268✔
52
    {
53
    }
268✔
54

55
    public override void Show(string title, string message)
56
    {
57
        Console.WriteLine(message);
8✔
58
    }
8✔
59

60
    public override bool TypeText(DialogArgs args, int maxLength, string initialText, out string text,
61
        bool requireSaneHeaderText)
62
    {
63
        text = AnsiConsole.Prompt(
×
64
            new TextPrompt<string>(GetPromptFor(args))
×
65
                .AllowEmpty()
×
66
        );
×
67

68
        if (string.Equals(text, "Cancel", StringComparison.CurrentCultureIgnoreCase))
×
69
            // user does not want to type any text
70
            return false;
×
71

72
        // user typed "null" or some spaces or something
73
        if (text.IsBasicallyNull())
×
74
            text = null;
×
75

76
        // that's still an affirmative choice
77
        return true;
×
78
    }
79

80
    public override DiscoveredDatabase SelectDatabase(bool allowDatabaseCreation, string taskDescription)
81
    {
82
        if (DisallowInput)
×
83
            throw new InputDisallowedException($"Value required for '{taskDescription}'");
×
84

85
        var value = ReadLineWithAuto(new DialogArgs { WindowTitle = taskDescription }, new PickDatabase());
×
86
        return value.Database;
×
87
    }
88

89
    public override DiscoveredTable SelectTable(bool allowDatabaseCreation, string taskDescription)
90
    {
91
        if (DisallowInput)
×
92
            throw new InputDisallowedException($"Value required for '{taskDescription}'");
×
93

94
        var value = ReadLineWithAuto(new DialogArgs { WindowTitle = taskDescription }, new PickTable());
×
95
        return value.Table;
×
96
    }
97

98
    public override void ShowException(string errorText, Exception exception)
99
    {
100
        throw exception ?? new Exception(errorText);
×
101
    }
102

103
    public override bool SelectEnum(DialogArgs args, Type enumType, out Enum chosen)
104
    {
105
        if (DisallowInput)
×
106
            throw new InputDisallowedException($"Value required for '{args}'");
×
107

108
        var chosenStr = GetString(args, Enum.GetNames(enumType).ToList());
×
109
        try
110
        {
111
            chosen = (Enum)Enum.Parse(enumType, chosenStr);
×
112
        }
×
113
        catch (Exception)
×
114
        {
115
            Console.WriteLine(
×
116
                $"Could not parse value.  Valid Enum values are:{Environment.NewLine}{string.Join(Environment.NewLine, Enum.GetNames(enumType))}");
×
117
            throw;
×
118
        }
119

120
        return true;
×
121
    }
122

123
    public override bool SelectType(DialogArgs args, Type[] available, out Type chosen)
124
    {
125
        if (DisallowInput)
×
126
            throw new InputDisallowedException($"Value required for '{args}'");
×
127

128
        var chosenStr = GetString(args, available.Select(t => t.Name).ToList());
×
129

130
        if (string.IsNullOrWhiteSpace(chosenStr))
×
131
        {
132
            chosen = null;
×
133
            return false;
×
134
        }
135

136
        chosen = available.SingleOrDefault(t => t.Name.Equals(chosenStr));
×
137

138
        return chosen == null ? throw new Exception($"Unknown or incompatible Type '{chosen}'") : true;
×
139
    }
140

141

142
    public override IMapsDirectlyToDatabaseTable[] SelectMany(DialogArgs args, Type arrayElementType,
143
        IMapsDirectlyToDatabaseTable[] availableObjects)
144
    {
145
        var value = ReadLineWithAuto(args, new PickObjectBase[]
×
146
            { new PickObjectByID(this), new PickObjectByName(this) });
×
147

148
        var unavailable = value.DatabaseEntities.Except(availableObjects).ToArray();
×
149

150
        return unavailable.Any()
×
151
            ? throw new Exception(
×
152
                $"The following objects were not among the listed available objects {string.Join(",", unavailable.Select(o => o.ToString()))}")
×
153
            : value.DatabaseEntities.ToArray();
×
154
    }
155

156
    /// <summary>
157
    /// Displays the text described in the prompt theming <paramref name="args"/>
158
    /// </summary>
159
    /// <param name="args"></param>
160
    /// <param name="entryLabel"></param>
161
    /// <param name="pickers"></param>
162
    /// <exception cref="InputDisallowedException">Thrown if <see cref="DisallowInput"/> is true</exception>
163
    private string GetPromptFor(DialogArgs args, bool entryLabel = true, params PickObjectBase[] pickers)
164
    {
165
        if (DisallowInput)
2!
166
            throw new InputDisallowedException($"Value required for '{args}'");
2✔
167

168
        var sb = new StringBuilder();
×
169

170
        if (!string.IsNullOrWhiteSpace(args.WindowTitle))
×
171
        {
172
            sb.Append(Markup.Escape(args.WindowTitle));
×
173

174
            if (entryLabel && !string.IsNullOrWhiteSpace(args.EntryLabel)) sb.Append(" - ");
×
175
        }
176

177
        if (entryLabel && !string.IsNullOrWhiteSpace(args.EntryLabel))
×
178
            sb.Append($"[green]{Markup.Escape(args.EntryLabel)}[/]");
×
179

180
        if (!string.IsNullOrWhiteSpace(args.TaskDescription))
×
181
        {
182
            sb.AppendLine();
×
183
            sb.Append($"[grey]{Markup.Escape(args.TaskDescription)}[/]");
×
184
        }
185

186
        foreach (var picker in pickers)
×
187
        {
188
            sb.AppendLine();
×
189
            sb.Append($"Format:[grey]{Markup.Escape(picker.Format)}[/]");
×
190

191
            if (picker.Examples.Any())
×
192
            {
193
                sb.AppendLine();
×
NEW
194
                sb.Append("Examples:");
×
195
                foreach (var example in picker.Examples)
×
196
                {
197
                    sb.AppendLine();
×
198
                    sb.Append($"[grey]{Markup.Escape(example)}[/]");
×
199
                }
200
            }
201

202
            sb.AppendLine();
×
203
            sb.Append(':');
×
204
        }
205

206
        return sb.ToString();
×
207
    }
208

209
    public override IMapsDirectlyToDatabaseTable SelectOne(DialogArgs args,
210
        IMapsDirectlyToDatabaseTable[] availableObjects)
211
    {
212
        if (DisallowInput)
×
213
            return args.AllowAutoSelect && availableObjects.Length == 1
×
214
                ? availableObjects[0]
×
215
                : throw new InputDisallowedException($"Value required for '{args}'");
×
216

217
        if (availableObjects.Length == 0)
×
218
            throw new Exception("No available objects found");
×
219

220
        //handle auto selection when there is one object
221
        if (availableObjects.Length == 1 && args.AllowAutoSelect)
×
222
            return availableObjects[0];
×
223

224
        Console.WriteLine(args.WindowTitle);
×
225

226
        Console.Write(args.EntryLabel);
×
227

228
        var value = ReadLineWithAuto(args, new PickObjectBase[]
×
229
            { new PickObjectByID(this), new PickObjectByName(this) });
×
230

231
        var chosen = value.DatabaseEntities?.SingleOrDefault();
×
232

233
        if (chosen == null)
×
234
            return null;
×
235

236
        return !availableObjects.Contains(chosen)
×
237
            ? throw new Exception("Picked object was not one of the listed available objects")
×
238
            : chosen;
×
239
    }
240

241
    public override bool SelectObject<T>(DialogArgs args, T[] available, out T selected)
242
    {
243
        for (var i = 0; i < available.Length; i++) Console.WriteLine($"{i}:{available[i]}");
×
244

245
        Console.Write(args.EntryLabel);
×
246

247
        var result = Console.ReadLine();
×
248

249
        if (int.TryParse(result, out var idx))
×
250
            if (idx >= 0 && idx < available.Length)
×
251
            {
252
                selected = available[idx];
×
253
                return true;
×
254
            }
255

256
        selected = default;
×
257
        return false;
×
258
    }
259

260
    private CommandLineObjectPickerArgumentValue ReadLineWithAuto(DialogArgs args, params PickObjectBase[] pickers)
261
    {
262
        if (DisallowInput)
×
263
            throw new InputDisallowedException("Value required");
×
264

265
        var line = AnsiConsole.Prompt(
×
266
            new TextPrompt<string>(
×
267
                GetPromptFor(args, true, pickers).Trim()));
×
268

269

270
        var cli = new CommandLineObjectPicker(new[] { line }, pickers);
×
271
        return cli[0];
×
272
    }
273

274
    public override DirectoryInfo SelectDirectory(string prompt)
275
    {
276
        if (DisallowInput)
×
277
            throw new InputDisallowedException($"Value required for '{prompt}'");
×
278

279
        var result = AnsiConsole.Prompt<string>(
×
280
            new TextPrompt<string>(
×
281
                    GetPromptFor(new DialogArgs
×
282
                    {
×
283
                        WindowTitle = "Select Directory",
×
284
                        EntryLabel = prompt
×
285
                    }))
×
286
                .AllowEmpty());
×
287

288
        return result.IsBasicallyNull() ? null : new DirectoryInfo(result);
×
289
    }
290

291
    public override FileInfo SelectFile(string prompt) => DisallowInput
×
292
        ? throw new InputDisallowedException($"Value required for '{prompt}'")
×
293
        : SelectFile(prompt, null, null);
×
294

295
    public override FileInfo SelectFile(string prompt, string patternDescription, string pattern)
296
    {
297
        if (DisallowInput)
×
298
            throw new InputDisallowedException($"Value required for '{prompt}'");
×
299

300
        var result = AnsiConsole.Prompt<string>(
×
301
            new TextPrompt<string>(
×
302
                    GetPromptFor(new DialogArgs
×
303
                    {
×
304
                        WindowTitle = "Select File",
×
305
                        EntryLabel = prompt
×
306
                    }))
×
307
                .AllowEmpty());
×
308

309
        return result.IsBasicallyNull() ? null : new FileInfo(result);
×
310
    }
311

312
    public override FileInfo[] SelectFiles(string prompt, string patternDescription, string pattern)
313
    {
314
        if (DisallowInput)
×
315
            throw new InputDisallowedException($"Value required for '{prompt}'");
×
316

317
        var file = AnsiConsole.Prompt<string>(
×
318
            new TextPrompt<string>(
×
319
                    GetPromptFor(new DialogArgs
×
320
                    {
×
321
                        WindowTitle = "Select File(s)",
×
322
                        TaskDescription = patternDescription,
×
323
                        EntryLabel = prompt
×
324
                    }))
×
325
                .AllowEmpty());
×
326

327
        if (file.IsBasicallyNull())
×
328
            return null;
×
329

330
        var asteriskIdx = file.IndexOf('*');
×
331

332
        if (asteriskIdx != -1)
×
333
        {
334
            var idxLastSlash =
×
335
                file.LastIndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
×
336

337
            if (idxLastSlash == -1 || asteriskIdx < idxLastSlash)
×
338
                throw new Exception("Wildcards are only supported at the file level");
×
339

340
            var searchPattern = file[(idxLastSlash + 1)..];
×
341
            var dirStr = file[..idxLastSlash];
×
342

343
            var dir = new DirectoryInfo(dirStr);
×
344

345
            return !dir.Exists
×
346
                ? throw new DirectoryNotFoundException($"Could not find directory:{dirStr}")
×
347
                : dir.GetFiles(searchPattern).ToArray();
×
348
        }
349

350
        return new[] { new FileInfo(file) };
×
351
    }
352

353

354
    protected override bool SelectValueTypeImpl(DialogArgs args, Type paramType, object initialValue, out object chosen)
355
    {
356
        chosen = UsefulStuff.ChangeType(AnsiConsole.Ask<string>(GetPromptFor(args)), paramType);
×
357
        return true;
×
358
    }
359

360
    public override bool YesNo(DialogArgs args, out bool chosen)
361
    {
362
        var result = GetString(args, new List<string> { "Yes", "No", "Cancel" });
×
363
        chosen = result == "Yes";
×
364
        //user made a non-cancel decision?
365
        return result != "Cancel" && !string.IsNullOrWhiteSpace(result);
×
366
    }
367

368
    public string GetString(DialogArgs args, List<string> options)
369
    {
370
        var chosen = AnsiConsole.Prompt(
2✔
371
            new SelectionPrompt<string>()
2✔
372
                .PageSize(10)
2✔
373
                .Title(GetPromptFor(args))
2✔
374
                .AddChoices(options)
2✔
375
        );
2✔
376

377
        return chosen;
×
378
    }
379

380
    public override void ShowData(IViewSQLAndResultsCollection collection)
381
    {
382
        var point = collection.GetDataAccessPoint();
×
383
        var db = DataAccessPortal.ExpectDatabase(point, DataAccessContext.InternalDataProcessing);
×
384

385
        var sql = collection.GetSql();
×
386

387
        var logger = NLog.LogManager.GetCurrentClassLogger();
×
388
        logger.Trace($"About to ShowData from Query:{Environment.NewLine}{sql}");
×
389

390
        var toRun = new ExtractTableVerbatim(db.Server, sql, Console.OpenStandardOutput(), ",", null);
×
391
        toRun.DoExtraction();
×
392
    }
×
393

394
    public override void ShowLogs(ILoggedActivityRootObject rootObject)
395
    {
396
        foreach (var load in GetLogs(rootObject).OrderByDescending(l => l.StartTime))
×
397
        {
398
            Console.WriteLine(load.Description);
×
399
            Console.WriteLine(load.StartTime);
×
400

401
            Console.WriteLine($"Errors:{load.Errors.Count}");
×
402

403
            foreach (var error in load.Errors)
×
404
            {
405
                error.GetSummary(out var title, out var body, out _, out _);
×
406

407
                Console.WriteLine($"\t{title}");
×
408
                Console.WriteLine($"\t{body}");
×
409
            }
410

411
            Console.WriteLine("Tables Loaded:");
×
412

413
            foreach (var t in load.TableLoadInfos)
×
414
            {
415
                Console.WriteLine($"\t{t}: I={t.Inserts:N0} U={t.Updates:N0} D={t.Deletes:N0}");
×
416

417
                foreach (var source in t.DataSources)
×
418
                    Console.WriteLine($"\t\tSource:{source.Source}");
×
419
            }
420

421
            Console.WriteLine("Progress:");
×
422

423
            foreach (var p in load.Progress) Console.WriteLine($"\t{p.Date} {p.Description}");
×
424
        }
425
    }
×
426

427
    public override void ShowGraph(AggregateConfiguration aggregate)
428
    {
429
        ShowData(new ViewAggregateExtractUICollection(aggregate));
×
430
    }
×
431

432
    public override bool SelectObjects<T>(DialogArgs args, T[] available, out T[] selected)
433
    {
434
        if (available.Length == 0)
×
435
        {
436
            selected = Array.Empty<T>();
×
437
            return true;
×
438
        }
439

440
        for (var i = 0; i < available.Length; i++) Console.WriteLine($"{i}:{available[i]}");
×
441

442
        var result = Console.ReadLine();
×
443

444
        if (string.IsNullOrWhiteSpace(result))
×
445
        {
446
            // selecting none is a valid user selection
447
            selected = Array.Empty<T>();
×
448
            return true;
×
449
        }
450

451
        var selectIdx = result.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
×
452

453
        selected = available.Where((e, idx) => selectIdx.Contains(idx)).ToArray();
×
454
        return true;
×
455
    }
456

457
    public override void LaunchSubprocess(ProcessStartInfo startInfo)
458
    {
459
        throw new NotSupportedException();
×
460
    }
461

462
    public override void Wait(string title, Task task, CancellationTokenSource cts)
463
    {
464
        AnsiConsole.Status()
4✔
465
            .Spinner(Spinner.Known.Star)
4✔
466
            .Start(title, ctx =>
4✔
467
                base.Wait(title, task, cts)
4✔
468
            );
4✔
469
    }
4✔
470

471
    public override void ShowData(DataTable collection)
472
    {
473
        var tbl = new Table();
×
474

475
        foreach (DataColumn c in collection.Columns) tbl.AddColumn(c.ColumnName);
×
476

477
        foreach (DataRow row in collection.Rows) tbl.AddRow(row.ItemArray.Select(i => i.ToString()).ToArray());
×
478
        AnsiConsole.Write(tbl);
×
479
    }
×
480
}
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