Skip to content

Commit

Permalink
Merge pull request #669 from arkfinn/refactor/plugin-runner
Browse files Browse the repository at this point in the history
Refactor PluginRunner to OpenUtau.Core and testable
  • Loading branch information
stakira authored May 14, 2023
2 parents 3f2cbe5 + c1fc38c commit c1a96c9
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 37 deletions.
6 changes: 6 additions & 0 deletions OpenUtau.Core/Classic/IPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OpenUtau.Classic {
public interface IPlugin {
string Encoding { get; }
void Run(string tempFile);
}
}
18 changes: 10 additions & 8 deletions OpenUtau.Core/Classic/Plugin.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;

namespace OpenUtau.Classic {
public class Plugin {
public class Plugin : IPlugin {
public string Name;
public string Executable;
public bool AllNotes;
public bool UseShell;
public string Encoding = "shift_jis";
private string encoding = "shift_jis";

public string Encoding { get => encoding; set => encoding = value; }

public void Run(string tempFile) {
if (!File.Exists(Executable)) {
throw new FileNotFoundException($"Executable {Executable} not found.");
}
var startInfo = new ProcessStartInfo() {
FileName = Executable,
Arguments = tempFile,
WorkingDirectory = Path.GetDirectoryName(Executable),
UseShellExecute = UseShell,
};
FileName = Executable,
Arguments = tempFile,
WorkingDirectory = Path.GetDirectoryName(Executable),
UseShellExecute = UseShell,
};
using (var process = Process.Start(startInfo)) {
process.WaitForExit();
}
Expand Down
100 changes: 100 additions & 0 deletions OpenUtau.Core/Classic/PluginRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using OpenUtau.Core;
using OpenUtau.Core.Ustx;
using Serilog;

namespace OpenUtau.Classic {
public class PluginRunner {
private readonly Action<ReplaceNoteEventArgs> OnReplaceNote;
private readonly Action<PluginErrorEventArgs> OnError;
private readonly PathManager PathManager;

public static PluginRunner from(PathManager pathManager, DocManager docManager) {
return new PluginRunner(pathManager, ReplaceNoteMethod(docManager), ShowErrorMessageMEthod(docManager));
}

private static Action<ReplaceNoteEventArgs> ReplaceNoteMethod(DocManager docManager) {
return new Action<ReplaceNoteEventArgs>((args) => {
docManager.StartUndoGroup();
docManager.ExecuteCmd(new RemoveNoteCommand(args.Part, args.ToRemove));
docManager.ExecuteCmd(new AddNoteCommand(args.Part, args.ToAdd));
docManager.EndUndoGroup();
});
}

private static Action<PluginErrorEventArgs> ShowErrorMessageMEthod(DocManager docManager) {
return new Action<PluginErrorEventArgs>((args) => {
docManager.ExecuteCmd(new ErrorMessageNotification(args.Message, args.Exception));
});
}

/// <summary>
/// for test
/// </summary>
/// <param name="pathManager"></param>
/// <param name="onReplaceNote"></param>
/// <param name="onError"></param>
public PluginRunner(PathManager pathManager, Action<ReplaceNoteEventArgs> onReplaceNote, Action<PluginErrorEventArgs> onError) {
PathManager = pathManager;
OnReplaceNote = onReplaceNote;
OnError = onError;
}

public void Execute(UProject project, UVoicePart part, UNote? first, UNote? last, IPlugin plugin) {
if (first == null || last == null) {
return;
}
try {
var tempFile = Path.Combine(PathManager.CachePath, "temp.tmp");
var sequence = Ust.WritePlugin(project, part, first, last, tempFile, encoding: plugin.Encoding);
byte[]? beforeHash = HashFile(tempFile);
plugin.Run(tempFile);
byte[]? afterHash = HashFile(tempFile);
if (beforeHash == null || afterHash == null || Enumerable.SequenceEqual(beforeHash, afterHash)) {
Log.Information("Legacy plugin temp file has not changed.");
return;
}
Log.Information("Legacy plugin temp file has changed.");
var (toRemove, toAdd) = Ust.ParsePlugin(project, part, first, last, sequence, tempFile, encoding: plugin.Encoding);
OnReplaceNote(new ReplaceNoteEventArgs(part, toRemove, toAdd));
} catch (Exception e) {
OnError(new PluginErrorEventArgs("Failed to execute plugin", e));
}
}


private byte[]? HashFile(string filePath) {
using (var md5 = MD5.Create()) {
using (var stream = File.OpenRead(filePath)) {
return md5.ComputeHash(stream);
}
}
}

public class ReplaceNoteEventArgs : EventArgs {
public readonly UVoicePart Part;
public readonly List<UNote> ToRemove;
public readonly List<UNote> ToAdd;

public ReplaceNoteEventArgs(UVoicePart part, List<UNote> toRemove, List<UNote> toAdd) {
Part = part;
ToRemove = toRemove;
ToAdd = toAdd;
}
}

public class PluginErrorEventArgs : EventArgs {
public readonly string Message;
public readonly Exception Exception;

public PluginErrorEventArgs(string message, Exception exception) {
Exception = exception;
Message = message;
}
}
}
}
179 changes: 179 additions & 0 deletions OpenUtau.Test/Classic/PluginRunnerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using OpenUtau.Core;
using OpenUtau.Core.Ustx;
using Xunit;
using static OpenUtau.Classic.PluginRunner;

namespace OpenUtau.Classic {


public class PluginRunnerTest {

class ExecuteTestData : IEnumerable<object[]> {
private readonly List<object[]> testData = new();

public ExecuteTestData() {
testData.Add(new object[] { BasicUProject(), IncludeNullResponse(), IncludeNullAssertion(), EmptyErrorMEthod() });
}

public IEnumerator<object[]> GetEnumerator() => testData.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public static ExecuteArgument BasicUProject() {
var project = new UProject();
project.tracks.Add(new UTrack {
TrackNo = 0,
});
var part = new UVoicePart() {
trackNo = 0,
position = 0,
};
project.parts.Add(part);

var before = UNote.Create();
before.lyric = "a";
before.duration = 10;

var first = UNote.Create();
first.lyric = "ka";
first.duration = 20;

var second = UNote.Create();
second.lyric = "r";
second.duration = 30;

var third = UNote.Create();
third.lyric = "ta";
third.duration = 40;

var last = UNote.Create();
last.lyric = "na";
last.duration = 50;

var after = UNote.Create();
after.lyric = "ha";
after.duration = 60;

part.notes.Add(before);
part.notes.Add(first);
part.notes.Add(second);
part.notes.Add(third);
part.notes.Add(last);
part.notes.Add(after);

return new ExecuteArgument(project, part, first, last);
}

private static Action<StreamWriter> IncludeNullResponse() {
return (writer) => {
// duration and lyric
writer.WriteLine("[#0000]");
writer.WriteLine("Length=480");
writer.WriteLine("Lyric=A");
writer.WriteLine("[#0001]");
writer.WriteLine("Length=480");
writer.WriteLine("Lyric=R");
// duration is null (change)
writer.WriteLine("[#0002]");
writer.WriteLine("Lyric=zo");
// duration is zero (delete)
writer.WriteLine("[#0003]");
writer.WriteLine("Length=");
// insert
writer.WriteLine("[#INSERT]");
writer.WriteLine("Length=240");
writer.WriteLine("Lyric=me");
};
}

private static Action<ReplaceNoteEventArgs> IncludeNullAssertion() {
return (args) => {
Assert.Equal(4, args.ToRemove.Count);
Assert.Equal(3, args.ToAdd.Count);
Assert.Equal(480, args.ToAdd[0].duration);
Assert.Equal("A", args.ToAdd[0].lyric);
Assert.Equal(40, args.ToAdd[1].duration);
Assert.Equal("zo", args.ToAdd[1].lyric);
Assert.Equal(240, args.ToAdd[2].duration);
Assert.Equal("me", args.ToAdd[2].lyric);
};
}

private static Action<PluginErrorEventArgs> EmptyErrorMEthod() {
return (args) => {
// do nothing
};
}
}

[Theory]
[ClassData(typeof(ExecuteTestData))]
public void ExecuteTest(ExecuteArgument given, Action<StreamWriter> when, Action<ReplaceNoteEventArgs> then, Action<PluginErrorEventArgs> error) {
// When
var action = new Action<PluginRunner>((runner) => {
runner.Execute(given.Project, given.Part, given.First, given.Last, new PluginStub(when));
});

// Then (Assert in ClassData)
action(new PluginRunner(PathManager.Inst, then, error));
}

[Fact]
public void ExecuteErrorTest() {
// Given
var given = ExecuteTestData.BasicUProject();

// When
var action = new Action<PluginRunner>((runner) => {
runner.Execute(given.Project, given.Part, given.First, given.Last, new PluginStub((writer) => {
// return empty text (invoke error)
}));
});

// Then
var then = new Action<ReplaceNoteEventArgs>(( args) => {
Assert.Fail("");
});
var error = new Action<PluginErrorEventArgs> ((args) => {
Assert.True(true);
});
action(new PluginRunner(PathManager.Inst, then,error));
}
}

class PluginStub : IPlugin {
public PluginStub(Action<StreamWriter> action) {
this.action = action;
}
private readonly Action<StreamWriter> action;

public string Encoding => "shift_jis";

public void Run(string tempFile) {
File.Delete(tempFile);
System.Text.Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
using (var writer = new StreamWriter(tempFile, false, System.Text.Encoding.GetEncoding(Encoding))) {
action.Invoke(writer);
}
}
}

public class ExecuteArgument {
public readonly UProject Project;
public readonly UVoicePart Part;
public readonly UNote First;
public readonly UNote Last;

public ExecuteArgument(UProject project, UVoicePart part, UNote first, UNote last) {
Project = project;
Part = part;
First = first;
Last = last;
}
}
}
41 changes: 12 additions & 29 deletions OpenUtau/ViewModels/PianoRollViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Security.Cryptography;
using Avalonia.Threading;
using DynamicData.Binding;
using OpenUtau.Classic;
using OpenUtau.Core;
using OpenUtau.Core.Editing;
using OpenUtau.Core.Ustx;
Expand Down Expand Up @@ -112,36 +113,18 @@ public PianoRollViewModel() {
if (NotesViewModel.Part == null || NotesViewModel.Part.notes.Count == 0) {
return;
}
try {
var project = NotesViewModel.Project;
var part = NotesViewModel.Part;
var tempFile = Path.Combine(PathManager.Inst.CachePath, "temp.tmp");
UNote? first = null;
UNote? last = null;
if (NotesViewModel.Selection.IsEmpty) {
first = part.notes.First();
last = part.notes.Last();
} else {
first = NotesViewModel.Selection.FirstOrDefault();
last = NotesViewModel.Selection.LastOrDefault();
}
var sequence = Classic.Ust.WritePlugin(project, part, first, last, tempFile, encoding: plugin.Encoding);
byte[]? beforeHash = HashFile(tempFile);
plugin.Run(tempFile);
byte[]? afterHash = HashFile(tempFile);
if (beforeHash == null || afterHash == null || Enumerable.SequenceEqual(beforeHash, afterHash)) {
Log.Information("Legacy plugin temp file has not changed.");
return;
}
Log.Information("Legacy plugin temp file has changed.");
var (toRemove, toAdd) = Classic.Ust.ParsePlugin(project, part, first, last, sequence, tempFile, encoding: plugin.Encoding);
DocManager.Inst.StartUndoGroup();
DocManager.Inst.ExecuteCmd(new RemoveNoteCommand(part, toRemove));
DocManager.Inst.ExecuteCmd(new AddNoteCommand(part, toAdd));
DocManager.Inst.EndUndoGroup();
} catch (Exception e) {
DocManager.Inst.ExecuteCmd(new ErrorMessageNotification("Failed to execute plugin", e));
var part = NotesViewModel.Part;
UNote? first;
UNote? last;
if (NotesViewModel.Selection.IsEmpty) {
first = part.notes.First();
last = part.notes.Last();
} else {
first = NotesViewModel.Selection.FirstOrDefault();
last = NotesViewModel.Selection.LastOrDefault();
}
var runner = PluginRunner.from(PathManager.Inst, DocManager.Inst);
runner.Execute(NotesViewModel.Project, part, first, last, plugin);
});
LegacyPlugins.AddRange(DocManager.Inst.Plugins.Select(plugin => new MenuItemViewModel() {
Header = plugin.Name,
Expand Down

0 comments on commit c1a96c9

Please sign in to comment.