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

HicServices / RDMP / 6245535001

20 Sep 2023 07:44AM UTC coverage: 57.013%. First build
6245535001

push

github

web-flow
8.1.0 Release (#1628)

* Bump Newtonsoft.Json from 13.0.1 to 13.0.2

Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 13.0.1 to 13.0.2.
- [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases)
- [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/13.0.1...13.0.2)

---
updated-dependencies:
- dependency-name: Newtonsoft.Json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump NLog from 5.0.5 to 5.1.0

Bumps [NLog](https://github.com/NLog/NLog) from 5.0.5 to 5.1.0.
- [Release notes](https://github.com/NLog/NLog/releases)
- [Changelog](https://github.com/NLog/NLog/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/NLog/NLog/compare/v5.0.5...v5.1.0)

---
updated-dependencies:
- dependency-name: NLog
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump NLog from 5.0.5 to 5.1.0

* Fix -r flag - should have been --results-directory all along

* Bump Newtonsoft.Json from 13.0.1 to 13.0.2

* Bump YamlDotNet from 12.0.2 to 12.1.0

Bumps [YamlDotNet](https://github.com/aaubry/YamlDotNet) from 12.0.2 to 12.1.0.
- [Release notes](https://github.com/aaubry/YamlDotNet/releases)
- [Commits](https://github.com/aaubry/YamlDotNet/compare/v12.0.2...v12.1.0)

---
updated-dependencies:
- dependency-name: YamlDotNet
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump Moq from 4.18.2 to 4.18.3

Bumps [Moq](https://github.com/moq/moq4) from 4.18.2 to 4.18.3.
- [Release notes](https://github.com/moq/moq4/releases)
- [Changelog](https://github.com/moq/moq4/blob/main/CHANGELOG.md)
- [Commits](https://github.com/moq/moq4/compare/v4.18.2...v4.18.3)

---
updated-dependencies:
- dependency-name: Moq
... (continued)

10732 of 20257 branches covered (0.0%)

Branch coverage included in aggregate %.

48141 of 48141 new or added lines in 1086 files covered. (100.0%)

30685 of 52388 relevant lines covered (58.57%)

7387.88 hits per line

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

46.73
/Rdmp.Core/Curation/Data/DataLoad/ProcessTask.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.Common;
10
using System.IO;
11
using System.Linq;
12
using System.Text.RegularExpressions;
13
using Rdmp.Core.Curation.Data.Cohort;
14
using Rdmp.Core.Curation.Data.ImportExport;
15
using Rdmp.Core.Curation.Data.Serialization;
16
using Rdmp.Core.MapsDirectlyToDatabaseTable;
17
using Rdmp.Core.MapsDirectlyToDatabaseTable.Attributes;
18
using Rdmp.Core.Repositories;
19
using Rdmp.Core.ReusableLibraryCode.Annotations;
20
using Rdmp.Core.ReusableLibraryCode.Checks;
21

22
namespace Rdmp.Core.Curation.Data.DataLoad;
23

24
/// <summary>
25
/// Describes a specific operation carried out at a specific step of a LoadMetadata.  This could be 'unzip all files called *.zip in for loading' or
26
/// 'after loading the data to live, call sp_clean_table1' or 'Connect to webservice X and download 1,000,000 records which will be serialized into XML'
27
/// 
28
/// <para>The class achieves this wide ranging functionality through the interaction of ProcessTaskType and Path.  e.g. when ProcessTaskType is Attacher then
29
/// Path functions as the Type name of a class that implements IAttacher e.g. 'LoadModules.Generic.Attachers.AnySeparatorFileAttacher'.  </para>
30
/// 
31
/// <para>Each ProcessTask can have one or more strongly typed arguments (see entity ProcessTaskArgument), these are discovered at design time by using
32
/// reflection to query the Path e.g. 'AnySeparatorFileAttacher' for all properties marked with [DemandsInitialization] attribute.  This allows for 3rd party developers
33
/// to write plugin classes to easily handle proprietary/bespoke source file types or complex data load requirements.</para>
34
/// </summary>
35
public class ProcessTask : DatabaseEntity, IProcessTask, IOrderable, INamed, ICheckable
36
{
37
    #region Database Properties
38

39
    private int _loadMetadataID;
40
    private int? _relatesSolelyToCatalogueID;
41
    private int _order;
42
    private string _path;
43
    private string _name;
44
    private LoadStage _loadStage;
45
    private ProcessTaskType _processTaskType;
46
    private bool _isDisabled;
47

48
    /// <summary>
49
    /// The load the process task exists as part of
50
    /// </summary>
51
    [Relationship(typeof(LoadMetadata), RelationshipType.SharedObject)]
52
    public int LoadMetadata_ID
53
    {
54
        get => _loadMetadataID;
2,232✔
55
        set => SetField(ref _loadMetadataID, value);
770✔
56
    }
57

58
    /// <inheritdoc/>
59
    [Obsolete(
60
        "Since you can't change which Catalogues are loaded by a LoadMetadata at runtime, this property is now obsolete")]
61
    public int? RelatesSolelyToCatalogue_ID
62
    {
63
        get => _relatesSolelyToCatalogueID;
536✔
64
        set => SetField(ref _relatesSolelyToCatalogueID, value);
146✔
65
    }
66

67
    /// <inheritdoc/>
68
    public int Order
69
    {
70
        get => _order;
1,098✔
71
        set => SetField(ref _order, value);
780✔
72
    }
73

74
    /// <inheritdoc/>
75
    [AdjustableLocation]
76
    public string Path
77
    {
78
        get => _path;
1,006✔
79
        set => SetField(ref _path, value);
848✔
80
    }
81

82
    /// <inheritdoc cref="IProcessTask.Name"/>
83
    [NotNull]
84
    public string Name
85
    {
86
        get => _name;
778✔
87
        set => SetField(ref _name, value);
844✔
88
    }
89

90
    /// <inheritdoc/>
91
    public LoadStage LoadStage
92
    {
93
        get => _loadStage;
1,660✔
94
        set => SetField(ref _loadStage, value);
796✔
95
    }
96

97
    /// <inheritdoc/>
98
    public ProcessTaskType ProcessTaskType
99
    {
100
        get => _processTaskType;
882✔
101
        set => SetField(ref _processTaskType, value);
890✔
102
    }
103

104
    /// <inheritdoc/>
105
    public bool IsDisabled
106
    {
107
        get => _isDisabled;
1,156✔
108
        set => SetField(ref _isDisabled, value);
726✔
109
    }
110

111
    #endregion
112

113
    #region Relationships
114

115
    /// <inheritdoc cref="LoadMetadata_ID"/>
116
    [NoMappingToDatabase]
117
    public LoadMetadata LoadMetadata => Repository.GetObjectByID<LoadMetadata>(LoadMetadata_ID);
168✔
118

119
    /// <inheritdoc/>
120
    [NoMappingToDatabase]
121
    public IEnumerable<ProcessTaskArgument> ProcessTaskArguments =>
122
        Repository.GetAllObjectsWithParent<ProcessTaskArgument>(this);
414✔
123

124
    /// <summary>
125
    /// All <see cref="ILoadProgress"/> (if any) that can be advanced by executing this load.  This allows batch execution of large loads
126
    /// </summary>
127
    [NoMappingToDatabase]
128
    public ILoadProgress[] LoadProgresses => LoadMetadata.LoadProgresses;
2✔
129

130
    #endregion
131

132
    public ProcessTask()
×
133
    {
134
    }
×
135

136
    /// <summary>
137
    /// Creates a new operation in the data load (e.g. copy files from A to B, load all CSV files to RAW table B etc)
138
    /// </summary>
139
    /// <param name="repository"></param>
140
    /// <param name="parent"></param>
141
    /// <param name="stage"></param>
142
    public ProcessTask(ICatalogueRepository repository, ILoadMetadata parent, LoadStage stage)
190✔
143
    {
144
        var order =
190✔
145
            repository.GetAllObjectsWithParent<ProcessTask>(parent).Select(t => t.Order).DefaultIfEmpty().Max() + 1;
202✔
146

147
        repository.InsertAndHydrate(this, new Dictionary<string, object>
190✔
148
        {
190✔
149
            { "LoadMetadata_ID", parent.ID },
190✔
150
            { "ProcessTaskType", ProcessTaskType.Executable.ToString() },
190✔
151
            { "LoadStage", stage },
190✔
152
            { "Name", $"New Process{Guid.NewGuid()}" },
190✔
153
            { "Order", order }
190✔
154
        });
190✔
155
    }
190✔
156

157
    internal ProcessTask(ICatalogueRepository repository, DbDataReader r)
158
        : base(repository, r)
570✔
159
    {
160
        LoadMetadata_ID = int.Parse(r["LoadMetaData_ID"].ToString());
570✔
161

162
        if (r["RelatesSolelyToCatalogue_ID"] != DBNull.Value)
570!
163
            _relatesSolelyToCatalogueID = int.Parse(r["RelatesSolelyToCatalogue_ID"].ToString());
×
164

165
        Path = r["Path"] as string;
570✔
166
        Name = r["Name"] as string;
570✔
167
        Order = int.Parse(r["Order"].ToString());
570✔
168

169
        if (Enum.TryParse(r["ProcessTaskType"] as string, out ProcessTaskType processTaskType))
570!
170
            ProcessTaskType = processTaskType;
570✔
171
        else
172
            throw new Exception($"Could not parse ProcessTaskType:{r["ProcessTaskType"]}");
×
173

174
        if (Enum.TryParse(r["LoadStage"] as string, out LoadStage loadStage))
570!
175
            LoadStage = loadStage;
570✔
176
        else
177
            throw new Exception($"Could not parse LoadStage:{r["LoadStage"]}");
×
178

179
        IsDisabled = Convert.ToBoolean(r["IsDisabled"]);
570✔
180
    }
570✔
181

182
    internal ProcessTask(ShareManager shareManager, ShareDefinition shareDefinition)
6✔
183
    {
184
        shareManager.UpsertAndHydrate(this, shareDefinition);
6✔
185
    }
6✔
186

187
    /// <inheritdoc/>
188
    public override string ToString() => Name;
70✔
189

190
    /// <inheritdoc/>
191
    public void Check(ICheckNotifier notifier)
192
    {
193
        switch (ProcessTaskType)
38!
194
        {
195
            case ProcessTaskType.Executable:
196
                CheckFileExistenceAndUniqueness(notifier);
×
197
                break;
×
198
            case ProcessTaskType.SQLFile:
199
                CheckFileExistenceAndUniqueness(notifier);
×
200
                CheckForProblemsInSQLFile(notifier);
×
201

202
                break;
×
203
            case ProcessTaskType.Attacher:
204
                break;
205
            case ProcessTaskType.DataProvider:
206
                break;
207
            case ProcessTaskType.MutilateDataTable:
208
                break;
209
            default:
210
                throw new ArgumentOutOfRangeException();
×
211
        }
212
    }
38✔
213

214
    private void CheckForProblemsInSQLFile(ICheckNotifier notifier)
215
    {
216
        try
217
        {
218
            var sql = File.ReadAllText(Path);
×
219

220
            //let's check for any SQL that indicates user is trying to modify a STAGING table in a RAW script (for example)
221
            foreach (var tableInfo in LoadMetadata.GetDistinctTableInfoList(false))
×
222
                //for each stage get all the object names that are in that stage
223
                foreach (var stage in new[] { LoadStage.AdjustRaw, LoadStage.AdjustStaging, LoadStage.PostLoad })
×
224
                {
225
                    //process task belongs in that stage anyway so nothing is prohibited
226
                    if (stage == (LoadStage == LoadStage.Mounting ? LoadStage.AdjustRaw : LoadStage))
×
227
                        continue;
228

229
                    //figure out what is prohibited
230
                    var prohibitedSql = tableInfo.GetQuerySyntaxHelper()
×
231
                        .EnsureFullyQualified(tableInfo.GetDatabaseRuntimeName(stage), null,
×
232
                            tableInfo.GetRuntimeName(stage));
×
233

234
                    //if we reference it, complain
235
                    if (sql.Contains(prohibitedSql))
×
236
                        notifier.OnCheckPerformed(
×
237
                            new CheckEventArgs(
×
238
                                $"Sql in file '{Path}' contains a reference to '{prohibitedSql}' which is prohibited since the ProcessTask ('{Name}') runs in LoadStage {LoadStage}",
×
239
                                CheckResult.Warning));
×
240
                }
241
        }
×
242
        catch (Exception e)
×
243
        {
244
            notifier.OnCheckPerformed(new CheckEventArgs($"Failed to check the contents of the SQL file '{Path}'",
×
245
                CheckResult.Fail, e));
×
246
        }
×
247
    }
×
248

249
    private void CheckFileExistenceAndUniqueness(ICheckNotifier notifier)
250
    {
251
        if (string.IsNullOrWhiteSpace(Path))
×
252
        {
253
            notifier.OnCheckPerformed(new CheckEventArgs($"No Path specified for ProcessTask '{Name}'",
×
254
                CheckResult.Fail));
×
255
            return;
×
256
        }
257

258
        notifier.OnCheckPerformed(!File.Exists(Path)
×
259
            ? new CheckEventArgs($"Could not find File '{Path}' for ProcessTask '{Name}'", CheckResult.Fail)
×
260
            : new CheckEventArgs($"Found File '{Path}'", CheckResult.Success));
×
261

262

263
        var matchingPaths = Repository.GetAllObjects<ProcessTask>().Where(pt => pt.Path.Equals(Path));
×
264
        foreach (var duplicate in matchingPaths.Except(new[] { this }))
×
265
            notifier.OnCheckPerformed(
×
266
                new CheckEventArgs(
×
267
                    $"ProcessTask '{duplicate}' (ID={duplicate.ID}) also uses file '{System.IO.Path.GetFileName(Path)}'",
×
268
                    CheckResult.Warning));
×
269

270
        //conflicting tokens in Name string
271
        foreach (Match match in Regex.Matches(Name, @"'.*((\.exe')|(\.sql'))"))
×
272
            if (match.Success)
×
273
            {
274
                var referencedFile = System.IO.Path.GetFileName(match.Value.Trim('\''));
×
275
                var actualFile = System.IO.Path.GetFileName(Path);
×
276

277
                if (referencedFile != actualFile)
×
278
                    notifier.OnCheckPerformed(
×
279
                        new CheckEventArgs(
×
280
                            $"Name of ProcessTask '{Name}' (ID={ID}) references file '{match.Value}' but the Path of the ProcessTask is '{Path}'",
×
281
                            CheckResult.Fail));
×
282
            }
283
    }
×
284

285
    /// <summary>
286
    /// Returns all tables loaded by the parent <see cref="LoadMetadata"/>
287
    /// </summary>
288
    /// <returns></returns>
289
    public IEnumerable<TableInfo> GetTableInfos() => LoadMetadata.GetDistinctTableInfoList(true);
2✔
290

291
    /// <inheritdoc/>
292
    public IEnumerable<IArgument> GetAllArguments() => ProcessTaskArguments;
276✔
293

294
    /// <inheritdoc/>
295
    public IArgument CreateNewArgument() => new ProcessTaskArgument((ICatalogueRepository)Repository, this);
1,198✔
296

297
    /// <inheritdoc/>
298
    public string GetClassNameWhoArgumentsAreFor() => Path;
18✔
299

300
    /// <summary>
301
    /// Creates a new copy of the processTask and all its arguments in the database, this clone is then hooked up to the
302
    /// new LoadMetadata at the specified stage
303
    /// </summary>
304
    /// <param name="loadMetadata">The new LoadMetadata parent for the clone</param>
305
    /// <param name="loadStage">The new load stage to put the clone in </param>
306
    /// <returns>the new ProcessTask (the clone has a different ID to the parent)</returns>
307
    public ProcessTask CloneToNewLoadMetadataStage(LoadMetadata loadMetadata, LoadStage loadStage)
308
    {
309
        var cataRepository = (ICatalogueRepository)Repository;
4✔
310

311
        //clone only accepts sql connections so make sure we aren't in mysql land or something
312
        using (cataRepository.BeginNewTransaction())
4✔
313
        {
314
            try
315
            {
316
                //get list of arguments to also clone (will happen outside of transaction
317
                var toCloneArguments = ProcessTaskArguments.ToArray();
4✔
318

319
                //create a new transaction for all the cloning - note that once all objects are cloned the transaction is committed then all the objects are adjusted outside the transaction
320
                var clone = new ProcessTask(CatalogueRepository, LoadMetadata, loadStage);
4✔
321
                CopyShallowValuesTo(clone);
4✔
322

323
                //foreach of our child arguments
324
                foreach (var argument in toCloneArguments)
12✔
325
                    //clone it but rewire it to the proper ProcessTask parent (the clone)
326
                    argument.ShallowClone(clone);
2✔
327

328
                //the values passed into parameter
329
                clone.LoadMetadata_ID = loadMetadata.ID;
4✔
330
                clone.LoadStage = loadStage;
4✔
331
                clone.SaveToDatabase();
4✔
332

333
                //it worked
334
                cataRepository.EndTransaction(true);
4✔
335

336
                //return the clone
337
                return clone;
4✔
338
            }
339
            catch (Exception)
×
340
            {
341
                cataRepository.EndTransaction(false);
×
342
                throw;
×
343
            }
344
        }
345
    }
4✔
346

347
    /// <inheritdoc/>
348
    public IArgument[] CreateArgumentsForClassIfNotExists(Type t) =>
349
        ArgumentFactory.CreateArgumentsForClassIfNotExistsGeneric(
70✔
350
                t,
70✔
351

70✔
352
                //tell it how to create new instances of us related to parent
70✔
353
                this,
70✔
354

70✔
355
                //what arguments already exist
70✔
356
                GetAllArguments().ToArray())
70✔
357

70✔
358
            //convert the result back from generic to specific (us)
70✔
359
            .ToArray();
70✔
360

361
    /// <inheritdoc/>
362
    public IArgument[] CreateArgumentsForClassIfNotExists<T>() => CreateArgumentsForClassIfNotExists(typeof(T));
66✔
363

364
    /// <summary>
365
    /// Returns true if the <see cref="ProcessTaskType"/> is allowed to happen during the given <see cref="LoadStage"/>  (e.g. you can't use an IAttacher to
366
    /// load data into STAGING/LIVE - only RAW).
367
    /// </summary>
368
    /// <param name="type"></param>
369
    /// <param name="stage"></param>
370
    /// <returns></returns>
371
    public static bool IsCompatibleStage(ProcessTaskType type, LoadStage stage)
372
    {
373
        return type switch
×
374
        {
×
375
            ProcessTaskType.Executable => true,
×
376
            ProcessTaskType.SQLFile => stage != LoadStage.GetFiles,
×
377
            ProcessTaskType.Attacher => stage == LoadStage.Mounting,
×
378
            ProcessTaskType.DataProvider => true,
×
379
            ProcessTaskType.MutilateDataTable => stage != LoadStage.GetFiles,
×
380
            _ => throw new ArgumentOutOfRangeException(nameof(type))
×
381
        };
×
382
    }
383

384
    /// <summary>
385
    /// True if <see cref="Path"/> is the name of a C# class (as opposed to the path to an executable or SQL file etc)
386
    /// </summary>
387
    /// <returns></returns>
388
    public bool IsPluginType() => ProcessTaskType == ProcessTaskType.Attacher ||
×
389
                                  ProcessTaskType == ProcessTaskType.MutilateDataTable ||
×
390
                                  ProcessTaskType == ProcessTaskType.DataProvider;
×
391

392
    /// <summary>
393
    /// Sets the value of the corresponding <see cref="IArgument"/> (which must already exist) to the given value.  If your argument doesn't exist yet you
394
    /// can call <see cref="CreateArgumentsForClassIfNotExists"/>
395
    /// </summary>
396
    /// <param name="parameterName"></param>
397
    /// <param name="o"></param>
398
    public void SetArgumentValue(string parameterName, object o)
399
    {
400
        var matchingArgument = ProcessTaskArguments.SingleOrDefault(p => p.Name.Equals(parameterName)) ??
2,280!
401
                               throw new Exception(
114✔
402
                                   $"Could not find a ProcessTaskArgument called '{parameterName}', have you called CreateArgumentsForClassIfNotExists<T> yet?");
114✔
403
        matchingArgument.SetValue(o);
114✔
404
        matchingArgument.SaveToDatabase();
114✔
405
    }
114✔
406
}
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