diff --git a/Src/WitsmlExplorer.Api/Jobs/DeleteEmptyMnemonicsJob.cs b/Src/WitsmlExplorer.Api/Jobs/DeleteEmptyMnemonicsJob.cs new file mode 100644 index 000000000..d900f3b23 --- /dev/null +++ b/Src/WitsmlExplorer.Api/Jobs/DeleteEmptyMnemonicsJob.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; + +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; + +namespace WitsmlExplorer.Api.Jobs +{ + public record DeleteEmptyMnemonicsJob : Job + { + + public IEnumerable Wells { get; init; } + public IEnumerable Wellbores { get; init; } + public double NullDepthValue { get; init; } + public DateTime NullTimeValue { get; init; } + + public DeleteEmptyMnemonicsJob() + { + Wells = new List(); + Wellbores = new List(); + } + + public override string Description() + { + return "DeleteEmptyMnemonicsJob" + + $" - WellUids: {GetWellUid()};" + + $" WellboreUids: {string.Join(", ", Wellbores.Select(w => w.WellboreUid))}"; + } + + public override string GetObjectName() + { + return null; + } + + public override string GetWellboreName() + { + return Wellbores.IsNullOrEmpty() ? null : string.Join(", ", Wellbores.Select(w => w.WellboreName)); + } + + public override string GetWellName() + { + var wellNames = new List(); + + if (!Wellbores.IsNullOrEmpty()) + { + wellNames.AddRange(Wellbores.Select(w => w.WellName).Distinct()); + } + + if (!Wells.IsNullOrEmpty()) + { + wellNames.AddRange(Wells.Select(w => w.WellName).Distinct()); + } + + return string.Join(", ", wellNames.Distinct()); + } + + private string GetWellUid() + { + var wellUids = new List(); + + if (!Wellbores.IsNullOrEmpty()) + { + wellUids.AddRange(Wellbores.Select(w => w.WellUid).Distinct()); + } + + if (!Wells.IsNullOrEmpty()) + { + wellUids.AddRange(Wells.Select(w => w.WellUid).Distinct()); + } + + return string.Join(", ", wellUids.Distinct()); + } + } +} diff --git a/Src/WitsmlExplorer.Api/Models/JobType.cs b/Src/WitsmlExplorer.Api/Models/JobType.cs index 82e2f2f29..92c87ec6a 100644 --- a/Src/WitsmlExplorer.Api/Models/JobType.cs +++ b/Src/WitsmlExplorer.Api/Models/JobType.cs @@ -16,6 +16,7 @@ public enum JobType DeleteWell, DeleteWellbore, RenameMnemonic, + DeleteEmptyMnemonics, ModifyBhaRun, ModifyFormationMarker, ModifyGeologyInterval, diff --git a/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs b/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs index f39631a69..7a0438186 100644 --- a/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs +++ b/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs @@ -6,16 +6,16 @@ namespace WitsmlExplorer.Api.Models { public class LogCurveInfo { - public string Uid { get; internal set; } - public string Mnemonic { get; internal set; } - public string MinDateTimeIndex { get; internal set; } - public string MinDepthIndex { get; internal set; } - public string MaxDateTimeIndex { get; internal set; } - public string MaxDepthIndex { get; internal set; } - public string ClassWitsml { get; internal set; } - public string Unit { get; internal set; } - public LengthMeasure SensorOffset { get; internal set; } - public string MnemAlias { get; internal set; } - public List AxisDefinitions { get; internal set; } + public string Uid { get; init; } + public string Mnemonic { get; init; } + public string MinDateTimeIndex { get; init; } + public string MinDepthIndex { get; init; } + public string MaxDateTimeIndex { get; init; } + public string MaxDepthIndex { get; init; } + public string ClassWitsml { get; init; } + public string Unit { get; init; } + public LengthMeasure SensorOffset { get; init; } + public string MnemAlias { get; init; } + public List AxisDefinitions { get; init; } } } diff --git a/Src/WitsmlExplorer.Api/Models/Reports/DeleteEmptyMnemonicsReport.cs b/Src/WitsmlExplorer.Api/Models/Reports/DeleteEmptyMnemonicsReport.cs new file mode 100644 index 000000000..b23cde686 --- /dev/null +++ b/Src/WitsmlExplorer.Api/Models/Reports/DeleteEmptyMnemonicsReport.cs @@ -0,0 +1,20 @@ +using WitsmlExplorer.Api.Jobs.Common; + +namespace WitsmlExplorer.Api.Models.Reports +{ + public class DeleteEmptyMnemonicsReport : BaseReport + { + } + + public class DeleteEmptyMnemonicsReportItem + { + public string WellName { get; init; } + public string WellUid { get; init; } + public string WellboreName { get; init; } + public string WellboreUid { get; init; } + public string LogName { get; init; } + public string LogUid { get; init; } + public string LogIndexType { get; init; } + public string Mnemonic { get; init; } + } +} diff --git a/Src/WitsmlExplorer.Api/Services/MnemonicService.cs b/Src/WitsmlExplorer.Api/Services/MnemonicService.cs new file mode 100644 index 000000000..39684012d --- /dev/null +++ b/Src/WitsmlExplorer.Api/Services/MnemonicService.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; + +using Witsml; + +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Query; + +namespace WitsmlExplorer.Api.Services +{ + public interface IMnemonicService + { + Task DeleteMnemonic(string wellUid, string wellboreUid, string logToCheckUid, LogCurveInfo mnemonicToDelete); + } + + public class MnemonicService : WitsmlService, IMnemonicService + { + public MnemonicService(IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider) + { + } + + public async Task DeleteMnemonic(string wellUid, string wellboreUid, string logToCheckUid, LogCurveInfo mnemonicToDelete) + { + var query = LogQueries.DeleteMnemonics(wellUid, wellboreUid, logToCheckUid, new[] { mnemonicToDelete.Mnemonic }); + + return await _witsmlClient.DeleteFromStoreAsync(query); + } + } +} diff --git a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteEmptyMnemonicsWorker.cs b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteEmptyMnemonicsWorker.cs new file mode 100644 index 000000000..96465ce1f --- /dev/null +++ b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteEmptyMnemonicsWorker.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; + +using Witsml; +using Witsml.Data; +using Witsml.Extensions; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Models.Reports; +using WitsmlExplorer.Api.Query; +using WitsmlExplorer.Api.Services; + +namespace WitsmlExplorer.Api.Workers.Delete +{ + public class DeleteEmptyMnemonicsWorker : BaseWorker, IWorker + { + public JobType JobType => JobType.DeleteEmptyMnemonics; + + private readonly IWellboreService _wellboreService; + private readonly ILogObjectService _logObjectService; + private readonly IMnemonicService _mnemonicService; + + public DeleteEmptyMnemonicsWorker( + ILogger logger, + IWitsmlClientProvider witsmlClientProvider, + IWellboreService wellboreService, + ILogObjectService logObjectService, + IMnemonicService mnemonicService) + : base(witsmlClientProvider, logger) + { + _wellboreService = wellboreService; + _logObjectService = logObjectService; + _mnemonicService = mnemonicService; + } + + public override async Task<(WorkerResult WorkerResult, RefreshAction RefreshAction)> Execute(DeleteEmptyMnemonicsJob job) + { + IWitsmlClient client = GetTargetWitsmlClientOrThrow(); + + var wellboreRefsToCheck = job.Wellbores.ToList(); + + wellboreRefsToCheck.AddRange(await ExtractWellboreRefs(job.Wells)); + + var reportItems = new List(); + var logCurvesCheckedCount = 0; + + foreach (var wellboreRef in wellboreRefsToCheck) + { + var logsToCheck = await ExtractLogs(wellboreRef); + + if (!logsToCheck.IsNullOrEmpty()) + { + foreach (var logToCheck in logsToCheck) + { + var logCurves = await GetLogCurveInfos(logToCheck); + + logCurvesCheckedCount += logCurves.Count(); + + var mnemonicsToDelete = FindNullMnemonics(job.NullDepthValue, job.NullTimeValue, logToCheck, logCurves); + + foreach (var mnemonicToDelete in mnemonicsToDelete) + { + var result = await DeleteMnemonic(wellboreRef.WellUid, wellboreRef.WellboreUid, logToCheck.Uid, mnemonicToDelete); + + var reportItem = new DeleteEmptyMnemonicsReportItem + { + WellName = wellboreRef.WellName, + WellUid = wellboreRef.WellUid, + WellboreName = wellboreRef.WellboreName, + WellboreUid = wellboreRef.WellboreUid, + LogName = logToCheck.Name, + LogUid = logToCheck.Uid, + LogIndexType = logToCheck.IndexType, + Mnemonic = mnemonicToDelete.Mnemonic + }; + + if (result.IsSuccessful) + { + reportItems.Add(reportItem); + + Logger.LogInformation("Successfully deleted empty mnemonic. WellUid: {WellUid}, WellboreUid: {WellboreUid}, Uid: {LogUid}, Mnemonic: {Mnemonic}" + , wellboreRef.WellUid, wellboreRef.WellboreUid, logToCheck.Uid, mnemonicToDelete.Mnemonic); + } + else + { + Logger.LogWarning("Failed to delete empty mnemonic. WellUid: {WellUid}, WellboreUid: {WellboreUid}, Uid: {LogUid}, Mnemonic: {Mnemonic}" + , wellboreRef.WellUid, wellboreRef.WellboreUid, logToCheck.Uid, mnemonicToDelete.Mnemonic); + } + } + } + } + } + + var report = new DeleteEmptyMnemonicsReport() + { + Title = "Delete Empty Mnemonics Report", + Summary = CreateReportSummary(job, logCurvesCheckedCount, reportItems.Count), + ReportItems = reportItems + }; + job.JobInfo.Report = report; + + Logger.LogInformation("{JobType} - Job successful. {Message}", GetType().Name, reportItems.IsNullOrEmpty() ? "No empty mnemonics deleted" : "Empty mnemonics deleted."); + + return ( + new WorkerResult(client.GetServerHostname(), true, $"Empty mnemonics deleted"), + null); + } + + private string CreateReportSummary(DeleteEmptyMnemonicsJob job, int mnemonicsCheckedCount, int mnemonicsDeletedCount) + { + var summary = new StringBuilder(); + + if (mnemonicsCheckedCount > 0) + { + switch (mnemonicsCheckedCount) + { + case 0: + summary = summary.AppendFormat("No mnemonics were"); + break; + case 1: + summary = summary.AppendFormat("One mnemonic was"); + break; + default: + summary = summary.AppendFormat("{0} mnemonics were", mnemonicsCheckedCount.ToString()); + break; + } + + summary = summary.AppendFormat(" checked for NullDepthValue: \"{0}\" and NullTimeValue: \"{1}\". ", + job.NullDepthValue.ToString(), + job.NullTimeValue.ToISODateTimeString()); + + switch (mnemonicsDeletedCount) + { + case 0: + summary = summary.AppendFormat("No empty mnemonics were found and deleted."); + break; + case 1: + summary = summary.AppendFormat("One empty mnemonic was found and deleted."); + break; + default: + summary = summary.AppendFormat("{0} empty mnemonics were found and deleted.", mnemonicsDeletedCount.ToString()); + break; + } + } + else + { + summary = summary.AppendFormat("No mnemonics were checked for NullDepthValue: \"{0}\" and NullTimeValue: \"{1}\".", + job.NullDepthValue.ToString(), + job.NullTimeValue.ToISODateTimeString()); + } + + return summary.ToString(); + } + + private async Task DeleteMnemonic(string wellUid, string wellboreUid, string logToCheckUid, LogCurveInfo mnemonicToDelete) + { + return await _mnemonicService.DeleteMnemonic(wellUid, wellboreUid, logToCheckUid, mnemonicToDelete); + } + + private ICollection FindNullMnemonics(double nullDepthValue, DateTime nullTimeValue, LogObject logToCheck, ICollection logCurves) + { + var nullMnemonics = new List(); + + var nullDepthValueString = nullDepthValue.ToString("G16", CultureInfo.InvariantCulture); + var nullTimeValueString = nullTimeValue.ToISODateTimeString(); + + if (!logCurves.IsNullOrEmpty()) + { + if (logToCheck.IndexType == WitsmlLog.WITSML_INDEX_TYPE_MD) + { + foreach (var logCurve in logCurves) + { + if (logCurve.MinDepthIndex == nullDepthValueString && logCurve.MaxDepthIndex == nullDepthValueString) + { + nullMnemonics.Add(logCurve); + } + } + } + else if (logToCheck.IndexType == WitsmlLog.WITSML_INDEX_TYPE_DATE_TIME) + { + foreach (var logCurve in logCurves) + { + if (logCurve.MinDateTimeIndex == nullTimeValueString && logCurve.MaxDateTimeIndex == nullTimeValueString) + { + nullMnemonics.Add(logCurve); + } + } + } + } + + return nullMnemonics; + } + + private async Task> GetLogCurveInfos(LogObject logToCheck) + { + return (await _logObjectService.GetLogCurveInfo(logToCheck.WellUid, logToCheck.WellboreUid, logToCheck.Uid)).ToList(); + } + + private async Task> ExtractLogs(WellboreReference wellboreRef) + { + return (await _logObjectService.GetLogs(wellboreRef.WellUid, wellboreRef.WellboreUid)).ToList(); + } + + private async Task> ExtractWellboreRefs(IEnumerable wellRefs) + { + var wellboreRefs = new List(); + + if (!wellRefs.IsNullOrEmpty()) + { + foreach (var wellRef in wellRefs) + { + var wellbores = await _wellboreService.GetWellbores(wellRef.WellUid); + + if (!wellbores.IsNullOrEmpty()) + { + wellboreRefs = wellboreRefs.Concat(wellbores.Select(wb => + new WellboreReference + { + WellboreUid = wb.Uid, + WellboreName = wb.Name, + WellUid = wb.WellUid, + WellName = wb.WellName + })) + .ToList(); + } + } + } + + return wellboreRefs; + } + } +} diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index 2e77d41e2..9561f83fa 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -19,6 +19,7 @@ import WellborePropertiesModal, { WellborePropertiesModalProps } from "../Modals import ContextMenu from "./ContextMenu"; import { StyledIcon } from "./ContextMenuUtils"; import NestedMenuItem from "./NestedMenuItem"; +import DeleteEmptyMnemonicsModal, { DeleteEmptyMnemonicsModalProps } from "../Modals/DeleteEmptyMnemonicsModal"; export interface WellContextMenuProps { dispatchOperation: (action: DisplayModalAction | HideModalAction | HideContextMenuAction) => void; @@ -90,6 +91,12 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { dispatchOperation({ type: OperationType.DisplayModal, payload: confirmation }); }; + const onClickDeleteEmptyMnemonics = async () => { + const deleteEmptyMnemonicsModalProps: DeleteEmptyMnemonicsModalProps = { wells: [well], dispatchOperation: dispatchOperation }; + const action: DisplayModalAction = { type: OperationType.DisplayModal, payload: }; + dispatchOperation(action); + }; + const onClickProperties = () => { const wellPropertiesModalProps: WellPropertiesModalProps = { mode: PropertiesModalMode.Edit, well, dispatchOperation }; dispatchOperation({ type: OperationType.DisplayModal, payload: }); @@ -122,6 +129,10 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { Delete , + + + Delete empty mnemonics + , {servers.map((server: Server) => ( onClickShowOnServer(server)}> diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx index 4732b6d01..dc2250137 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx @@ -27,6 +27,7 @@ import { StyledIcon, menuItemText } from "./ContextMenuUtils"; import { pasteObjectOnWellbore } from "./CopyUtils"; import NestedMenuItem from "./NestedMenuItem"; import { useClipboardReferences } from "./UseClipboardReferences"; +import DeleteEmptyMnemonicsModal, { DeleteEmptyMnemonicsModalProps } from "../Modals/DeleteEmptyMnemonicsModal"; export interface WellboreContextMenuProps { wellbore: Wellbore; @@ -107,6 +108,12 @@ const WellboreContextMenu = (props: WellboreContextMenuProps): React.ReactElemen dispatchOperation({ type: OperationType.DisplayModal, payload: confirmation }); }; + const onClickDeleteEmptyMnemonics = async () => { + const deleteEmptyMnemonicsModalProps: DeleteEmptyMnemonicsModalProps = { wellbores: [wellbore], dispatchOperation: dispatchOperation }; + const action: DisplayModalAction = { type: OperationType.DisplayModal, payload: }; + dispatchOperation(action); + }; + const onClickRefresh = async () => { dispatchOperation({ type: OperationType.HideContextMenu }); // toggle the wellbore node and navigate to parent wellbore to reset the sidebar and content view @@ -170,6 +177,10 @@ const WellboreContextMenu = (props: WellboreContextMenuProps): React.ReactElemen Delete , + + + Delete empty mnemonics + , {servers.map((server: Server) => ( onClickShowOnServer(server)}> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx new file mode 100644 index 000000000..b700398f5 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx @@ -0,0 +1,84 @@ +import Wellbore from "../../models/wellbore"; +import Well from "../../models/well"; +import JobService, { JobType } from "../../services/jobService"; +import { TextField } from "@material-ui/core"; +import OperationType from "../../contexts/operationType"; +import { useContext, useState } from "react"; +import { HideModalAction } from "../../contexts/operationStateReducer"; +import { DeleteEmptyMnemonicsJob } from "../../models/jobs/deleteEmptyMnemonicsJob"; +import ModalDialog from "./ModalDialog"; +import OperationContext from "../../contexts/operationContext"; +import { DateTimeField } from "./DateTimeField"; +import styled from "styled-components"; + +export interface DeleteEmptyMnemonicsModalProps { + wells?: Well[]; + wellbores?: Wellbore[]; + dispatchOperation: (action: HideModalAction) => void; +} + +const DeleteEmptyMnemonicsModal = (props: DeleteEmptyMnemonicsModalProps): React.ReactElement => { + const { wells, wellbores, dispatchOperation } = props; + const { + operationState: { timeZone } + } = useContext(OperationContext); + const [nullDepthValue, setNullDepthValue] = useState(-999.25); + const [nullTimeValue, setNullTimeValue] = useState("1900-01-01T00:00:00.000Z"); + const [nullTimeValueValid, setNullTimeValueValid] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const onSubmit = async (nullDepthValue: number, nullTimeValue: string) => { + setIsLoading(true); + + const job: DeleteEmptyMnemonicsJob = { + wells: wells?.map((x) => { + return { wellUid: x.uid, wellName: x.name }; + }), + wellbores: wellbores?.map((x) => { + return { wellboreUid: x.uid, wellboreName: x.name, wellUid: x.wellUid, wellName: x.wellName }; + }), + nullDepthValue: nullDepthValue, + nullTimeValue: nullTimeValue + }; + + await JobService.orderJob(JobType.DeleteEmptyMnemonics, job); + + setIsLoading(false); + + dispatchOperation({ type: OperationType.HideModal }); + }; + + return ( + <> + + { + setNullTimeValue(dateTime); + setNullTimeValueValid(valid); + }} + timeZone={timeZone} + /> + setNullDepthValue(+e.target.value)} /> + + } + confirmDisabled={!nullTimeValueValid} + onSubmit={() => onSubmit(nullDepthValue, nullTimeValue)} + isLoading={isLoading} + /> + + ); +}; + +const ContentLayout = styled.div` + display: flex; + flex-direction: column; + gap: 0.25rem; +`; + +export default DeleteEmptyMnemonicsModal; diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/deleteEmptyMnemonicsJob.ts b/Src/WitsmlExplorer.Frontend/models/jobs/deleteEmptyMnemonicsJob.ts new file mode 100644 index 000000000..616dd045b --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/jobs/deleteEmptyMnemonicsJob.ts @@ -0,0 +1,9 @@ +import WellboreReference from "./wellboreReference"; +import WellReference from "./wellReference"; + +export interface DeleteEmptyMnemonicsJob { + wells: WellReference[]; + wellbores: WellboreReference[]; + nullDepthValue: number; + nullTimeValue: string; +} diff --git a/Src/WitsmlExplorer.Frontend/services/jobService.tsx b/Src/WitsmlExplorer.Frontend/services/jobService.tsx index 09c5e9f10..f10935b14 100644 --- a/Src/WitsmlExplorer.Frontend/services/jobService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/jobService.tsx @@ -91,6 +91,7 @@ export enum JobType { ModifyTrajectory = "ModifyTrajectory", ModifyRisk = "ModifyRisk", RenameMnemonic = "RenameMnemonic", + DeleteEmptyMnemonics = "DeleteEmptyMnemonics", ModifyTrajectoryStation = "ModifyTrajectoryStation", ModifyTubular = "ModifyTubular", ModifyTubularComponent = "ModifyTubularComponent", diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteEmptyMnemonicsWorkerTest.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteEmptyMnemonicsWorkerTest.cs new file mode 100644 index 000000000..6a7a61260 --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteEmptyMnemonicsWorkerTest.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Serilog; + +using Witsml; +using Witsml.Data; +using Witsml.Extensions; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; +using WitsmlExplorer.Api.Workers.Delete; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class DeleteEmptyMnemonicsWorkerTest + { + private readonly Mock _witsmlClient; + private readonly DeleteEmptyMnemonicsWorker _worker; + private readonly Mock _wellboreService; + private readonly Mock _logObjectService; + private readonly Mock _mnemonicService; + + public DeleteEmptyMnemonicsWorkerTest() + { + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(Log.Logger); + ILogger logger = loggerFactory.CreateLogger(); + + Mock witsmlClientProvider = new(); + _witsmlClient = new Mock(); + witsmlClientProvider.Setup(wcp => wcp.GetClient()).Returns(_witsmlClient.Object); + + _wellboreService = new(); + _wellboreService + .Setup(ws => ws.GetWellbores(It.IsAny())) + .Returns(Task.Run(() => new List().AsEnumerable())); + + _logObjectService = new(); + + _mnemonicService = new(); + _mnemonicService + .Setup(ms => ms.DeleteMnemonic(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.Run(() => new QueryResult(true))); + + _worker = new DeleteEmptyMnemonicsWorker(logger, witsmlClientProvider.Object, _wellboreService.Object, _logObjectService.Object, _mnemonicService.Object); + } + + [Fact] + public async Task DeleteZeroMnemonics() + { + SetupDateTimeLogObject(); + + var dateTime = new DateTime(2023, 8, 20, 12, 0, 0); + + var job = CreateJob(10, dateTime); + + (WorkerResult result, RefreshAction _) = await _worker.Execute(job); + + _wellboreService.Verify(s => s.GetWellbores(It.IsAny()), Times.Never); + + _mnemonicService.Verify(s => s.DeleteMnemonic(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + Assert.True(result.IsSuccess); + + Assert.NotNull(job.JobInfo?.Report); + Assert.Empty(job.JobInfo.Report.ReportItems); + Assert.Equal("3 mnemonics were checked for NullDepthValue: \"10\" and NullTimeValue: \"" + dateTime.ToISODateTimeString() + "\". No empty mnemonics were found and deleted.", + job.JobInfo.Report.Summary); + + Assert.Equal("DeleteEmptyMnemonicsJob - WellUids: 111; WellboreUids: 112", job.JobInfo.Description); + Assert.Equal("Well111", job.JobInfo.WellName); + Assert.Equal("Wellbore112", job.JobInfo.WellboreName); + } + + [Fact] + public async Task DeleteOneMnemonic() + { + SetupDepthLogObject(); + + var dateTime = new DateTime(2023, 8, 20, 12, 0, 0); + + var job = CreateJob(0, dateTime); + + (WorkerResult result, RefreshAction _) = await _worker.Execute(job); + + _wellboreService.Verify(s => s.GetWellbores(It.IsAny()), Times.Never); + + _mnemonicService.Verify(s => s.DeleteMnemonic(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + + Assert.True(result.IsSuccess); + + Assert.NotNull(job.JobInfo?.Report); + Assert.Single(job.JobInfo.Report.ReportItems); + Assert.Equal("3 mnemonics were checked for NullDepthValue: \"0\" and NullTimeValue: \"" + dateTime.ToISODateTimeString() + "\". One empty mnemonic was found and deleted.", + job.JobInfo.Report.Summary); + + Assert.Equal("DeleteEmptyMnemonicsJob - WellUids: 111; WellboreUids: 112", job.JobInfo.Description); + Assert.Equal("Well111", job.JobInfo.WellName); + Assert.Equal("Wellbore112", job.JobInfo.WellboreName); + } + + [Fact] + public async Task DeleteTwoMnemonics() + { + SetupDateTimeLogObject(); + + var dateTime = new DateTime(2023, 3, 21, 12, 0, 0); + + var job = CreateJob(10, dateTime); + + (WorkerResult result, RefreshAction _) = await _worker.Execute(job); + + _wellboreService.Verify(s => s.GetWellbores(It.IsAny()), Times.Never); + + _mnemonicService.Verify(s => s.DeleteMnemonic(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(2)); + + Assert.True(result.IsSuccess); + + Assert.NotNull(job.JobInfo?.Report); + Assert.Equal(2, job.JobInfo.Report.ReportItems.Count()); + Assert.Equal("3 mnemonics were checked for NullDepthValue: \"10\" and NullTimeValue: \"" + dateTime.ToISODateTimeString() + "\". 2 empty mnemonics were found and deleted.", + job.JobInfo.Report.Summary); + + Assert.Equal("DeleteEmptyMnemonicsJob - WellUids: 111; WellboreUids: 112", job.JobInfo.Description); + Assert.Equal("Well111", job.JobInfo.WellName); + Assert.Equal("Wellbore112", job.JobInfo.WellboreName); + } + + private void SetupDateTimeLogObject() + { + _logObjectService + .Setup(los => los.GetLogs(It.IsAny(), It.IsAny())) + .Returns(Task.Run(() => new List { new LogObject() { Uid = "123", IndexType = WitsmlLog.WITSML_INDEX_TYPE_DATE_TIME } }.AsEnumerable())); + + var lcis = new List(); + + var lci = new LogCurveInfo + { + MinDateTimeIndex = new DateTime(2023, 1, 20, 12, 0, 0).ToISODateTimeString(), + MaxDateTimeIndex = new DateTime(2023, 2, 21, 12, 0, 0).ToISODateTimeString() + }; + lcis.Add(lci); + + lci = new LogCurveInfo + { + MinDateTimeIndex = new DateTime(2023, 3, 21, 12, 0, 0).ToISODateTimeString(), + MaxDateTimeIndex = new DateTime(2023, 3, 21, 12, 0, 0).ToISODateTimeString() + }; + lcis.Add(lci); + + lci = new LogCurveInfo + { + MinDateTimeIndex = new DateTime(2023, 3, 21, 12, 0, 0).ToISODateTimeString(), + MaxDateTimeIndex = new DateTime(2023, 3, 21, 12, 0, 0).ToISODateTimeString() + }; + lcis.Add(lci); + + _logObjectService + .Setup(los => los.GetLogCurveInfo(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.Run(() => lcis.AsEnumerable())); + } + + private void SetupDepthLogObject() + { + _logObjectService + .Setup(los => los.GetLogs(It.IsAny(), It.IsAny())) + .Returns(Task.Run(() => new List { new LogObject() { Uid = "123", IndexType = WitsmlLog.WITSML_INDEX_TYPE_MD } }.AsEnumerable())); + + var lcis = new List(); + + var lci = new LogCurveInfo + { + MinDepthIndex = "0", + MaxDepthIndex = "0" + }; + lcis.Add(lci); + + lci = new LogCurveInfo + { + MinDepthIndex = "1", + MaxDepthIndex = "1" + }; + lcis.Add(lci); + + lci = new LogCurveInfo + { + MinDepthIndex = "0", + MaxDepthIndex = "1" + }; + lcis.Add(lci); + + _logObjectService + .Setup(los => los.GetLogCurveInfo(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.Run(() => lcis.AsEnumerable())); + } + + private DeleteEmptyMnemonicsJob CreateJob(double nullDepthValue, DateTime nullTimeValue) + { + return new DeleteEmptyMnemonicsJob() + { + NullDepthValue = nullDepthValue, + NullTimeValue = nullTimeValue, + Wells = new List(), + Wellbores = new List { new WellboreReference() { WellUid = "111", WellName = "Well111", WellboreUid = "112", WellboreName = "Wellbore112" } }, + JobInfo = new JobInfo() + }; + } + } +}