Skip to content

Commit c1a96c9

Browse files
authored
Merge pull request #669 from arkfinn/refactor/plugin-runner
Refactor PluginRunner to OpenUtau.Core and testable
2 parents 3f2cbe5 + c1fc38c commit c1a96c9

File tree

5 files changed

+307
-37
lines changed

5 files changed

+307
-37
lines changed

OpenUtau.Core/Classic/IPlugin.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace OpenUtau.Classic {
2+
public interface IPlugin {
3+
string Encoding { get; }
4+
void Run(string tempFile);
5+
}
6+
}

OpenUtau.Core/Classic/Plugin.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
using System.Diagnostics;
1+
using System.Diagnostics;
22
using System.IO;
33

44
namespace OpenUtau.Classic {
5-
public class Plugin {
5+
public class Plugin : IPlugin {
66
public string Name;
77
public string Executable;
88
public bool AllNotes;
99
public bool UseShell;
10-
public string Encoding = "shift_jis";
10+
private string encoding = "shift_jis";
11+
12+
public string Encoding { get => encoding; set => encoding = value; }
1113

1214
public void Run(string tempFile) {
1315
if (!File.Exists(Executable)) {
1416
throw new FileNotFoundException($"Executable {Executable} not found.");
1517
}
1618
var startInfo = new ProcessStartInfo() {
17-
FileName = Executable,
18-
Arguments = tempFile,
19-
WorkingDirectory = Path.GetDirectoryName(Executable),
20-
UseShellExecute = UseShell,
21-
};
19+
FileName = Executable,
20+
Arguments = tempFile,
21+
WorkingDirectory = Path.GetDirectoryName(Executable),
22+
UseShellExecute = UseShell,
23+
};
2224
using (var process = Process.Start(startInfo)) {
2325
process.WaitForExit();
2426
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Security.Cryptography;
6+
using OpenUtau.Core;
7+
using OpenUtau.Core.Ustx;
8+
using Serilog;
9+
10+
namespace OpenUtau.Classic {
11+
public class PluginRunner {
12+
private readonly Action<ReplaceNoteEventArgs> OnReplaceNote;
13+
private readonly Action<PluginErrorEventArgs> OnError;
14+
private readonly PathManager PathManager;
15+
16+
public static PluginRunner from(PathManager pathManager, DocManager docManager) {
17+
return new PluginRunner(pathManager, ReplaceNoteMethod(docManager), ShowErrorMessageMEthod(docManager));
18+
}
19+
20+
private static Action<ReplaceNoteEventArgs> ReplaceNoteMethod(DocManager docManager) {
21+
return new Action<ReplaceNoteEventArgs>((args) => {
22+
docManager.StartUndoGroup();
23+
docManager.ExecuteCmd(new RemoveNoteCommand(args.Part, args.ToRemove));
24+
docManager.ExecuteCmd(new AddNoteCommand(args.Part, args.ToAdd));
25+
docManager.EndUndoGroup();
26+
});
27+
}
28+
29+
private static Action<PluginErrorEventArgs> ShowErrorMessageMEthod(DocManager docManager) {
30+
return new Action<PluginErrorEventArgs>((args) => {
31+
docManager.ExecuteCmd(new ErrorMessageNotification(args.Message, args.Exception));
32+
});
33+
}
34+
35+
/// <summary>
36+
/// for test
37+
/// </summary>
38+
/// <param name="pathManager"></param>
39+
/// <param name="onReplaceNote"></param>
40+
/// <param name="onError"></param>
41+
public PluginRunner(PathManager pathManager, Action<ReplaceNoteEventArgs> onReplaceNote, Action<PluginErrorEventArgs> onError) {
42+
PathManager = pathManager;
43+
OnReplaceNote = onReplaceNote;
44+
OnError = onError;
45+
}
46+
47+
public void Execute(UProject project, UVoicePart part, UNote? first, UNote? last, IPlugin plugin) {
48+
if (first == null || last == null) {
49+
return;
50+
}
51+
try {
52+
var tempFile = Path.Combine(PathManager.CachePath, "temp.tmp");
53+
var sequence = Ust.WritePlugin(project, part, first, last, tempFile, encoding: plugin.Encoding);
54+
byte[]? beforeHash = HashFile(tempFile);
55+
plugin.Run(tempFile);
56+
byte[]? afterHash = HashFile(tempFile);
57+
if (beforeHash == null || afterHash == null || Enumerable.SequenceEqual(beforeHash, afterHash)) {
58+
Log.Information("Legacy plugin temp file has not changed.");
59+
return;
60+
}
61+
Log.Information("Legacy plugin temp file has changed.");
62+
var (toRemove, toAdd) = Ust.ParsePlugin(project, part, first, last, sequence, tempFile, encoding: plugin.Encoding);
63+
OnReplaceNote(new ReplaceNoteEventArgs(part, toRemove, toAdd));
64+
} catch (Exception e) {
65+
OnError(new PluginErrorEventArgs("Failed to execute plugin", e));
66+
}
67+
}
68+
69+
70+
private byte[]? HashFile(string filePath) {
71+
using (var md5 = MD5.Create()) {
72+
using (var stream = File.OpenRead(filePath)) {
73+
return md5.ComputeHash(stream);
74+
}
75+
}
76+
}
77+
78+
public class ReplaceNoteEventArgs : EventArgs {
79+
public readonly UVoicePart Part;
80+
public readonly List<UNote> ToRemove;
81+
public readonly List<UNote> ToAdd;
82+
83+
public ReplaceNoteEventArgs(UVoicePart part, List<UNote> toRemove, List<UNote> toAdd) {
84+
Part = part;
85+
ToRemove = toRemove;
86+
ToAdd = toAdd;
87+
}
88+
}
89+
90+
public class PluginErrorEventArgs : EventArgs {
91+
public readonly string Message;
92+
public readonly Exception Exception;
93+
94+
public PluginErrorEventArgs(string message, Exception exception) {
95+
Exception = exception;
96+
Message = message;
97+
}
98+
}
99+
}
100+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Text;
6+
using OpenUtau.Core;
7+
using OpenUtau.Core.Ustx;
8+
using Xunit;
9+
using static OpenUtau.Classic.PluginRunner;
10+
11+
namespace OpenUtau.Classic {
12+
13+
14+
public class PluginRunnerTest {
15+
16+
class ExecuteTestData : IEnumerable<object[]> {
17+
private readonly List<object[]> testData = new();
18+
19+
public ExecuteTestData() {
20+
testData.Add(new object[] { BasicUProject(), IncludeNullResponse(), IncludeNullAssertion(), EmptyErrorMEthod() });
21+
}
22+
23+
public IEnumerator<object[]> GetEnumerator() => testData.GetEnumerator();
24+
25+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
26+
27+
public static ExecuteArgument BasicUProject() {
28+
var project = new UProject();
29+
project.tracks.Add(new UTrack {
30+
TrackNo = 0,
31+
});
32+
var part = new UVoicePart() {
33+
trackNo = 0,
34+
position = 0,
35+
};
36+
project.parts.Add(part);
37+
38+
var before = UNote.Create();
39+
before.lyric = "a";
40+
before.duration = 10;
41+
42+
var first = UNote.Create();
43+
first.lyric = "ka";
44+
first.duration = 20;
45+
46+
var second = UNote.Create();
47+
second.lyric = "r";
48+
second.duration = 30;
49+
50+
var third = UNote.Create();
51+
third.lyric = "ta";
52+
third.duration = 40;
53+
54+
var last = UNote.Create();
55+
last.lyric = "na";
56+
last.duration = 50;
57+
58+
var after = UNote.Create();
59+
after.lyric = "ha";
60+
after.duration = 60;
61+
62+
part.notes.Add(before);
63+
part.notes.Add(first);
64+
part.notes.Add(second);
65+
part.notes.Add(third);
66+
part.notes.Add(last);
67+
part.notes.Add(after);
68+
69+
return new ExecuteArgument(project, part, first, last);
70+
}
71+
72+
private static Action<StreamWriter> IncludeNullResponse() {
73+
return (writer) => {
74+
// duration and lyric
75+
writer.WriteLine("[#0000]");
76+
writer.WriteLine("Length=480");
77+
writer.WriteLine("Lyric=A");
78+
writer.WriteLine("[#0001]");
79+
writer.WriteLine("Length=480");
80+
writer.WriteLine("Lyric=R");
81+
// duration is null (change)
82+
writer.WriteLine("[#0002]");
83+
writer.WriteLine("Lyric=zo");
84+
// duration is zero (delete)
85+
writer.WriteLine("[#0003]");
86+
writer.WriteLine("Length=");
87+
// insert
88+
writer.WriteLine("[#INSERT]");
89+
writer.WriteLine("Length=240");
90+
writer.WriteLine("Lyric=me");
91+
};
92+
}
93+
94+
private static Action<ReplaceNoteEventArgs> IncludeNullAssertion() {
95+
return (args) => {
96+
Assert.Equal(4, args.ToRemove.Count);
97+
Assert.Equal(3, args.ToAdd.Count);
98+
Assert.Equal(480, args.ToAdd[0].duration);
99+
Assert.Equal("A", args.ToAdd[0].lyric);
100+
Assert.Equal(40, args.ToAdd[1].duration);
101+
Assert.Equal("zo", args.ToAdd[1].lyric);
102+
Assert.Equal(240, args.ToAdd[2].duration);
103+
Assert.Equal("me", args.ToAdd[2].lyric);
104+
};
105+
}
106+
107+
private static Action<PluginErrorEventArgs> EmptyErrorMEthod() {
108+
return (args) => {
109+
// do nothing
110+
};
111+
}
112+
}
113+
114+
[Theory]
115+
[ClassData(typeof(ExecuteTestData))]
116+
public void ExecuteTest(ExecuteArgument given, Action<StreamWriter> when, Action<ReplaceNoteEventArgs> then, Action<PluginErrorEventArgs> error) {
117+
// When
118+
var action = new Action<PluginRunner>((runner) => {
119+
runner.Execute(given.Project, given.Part, given.First, given.Last, new PluginStub(when));
120+
});
121+
122+
// Then (Assert in ClassData)
123+
action(new PluginRunner(PathManager.Inst, then, error));
124+
}
125+
126+
[Fact]
127+
public void ExecuteErrorTest() {
128+
// Given
129+
var given = ExecuteTestData.BasicUProject();
130+
131+
// When
132+
var action = new Action<PluginRunner>((runner) => {
133+
runner.Execute(given.Project, given.Part, given.First, given.Last, new PluginStub((writer) => {
134+
// return empty text (invoke error)
135+
}));
136+
});
137+
138+
// Then
139+
var then = new Action<ReplaceNoteEventArgs>(( args) => {
140+
Assert.Fail("");
141+
});
142+
var error = new Action<PluginErrorEventArgs> ((args) => {
143+
Assert.True(true);
144+
});
145+
action(new PluginRunner(PathManager.Inst, then,error));
146+
}
147+
}
148+
149+
class PluginStub : IPlugin {
150+
public PluginStub(Action<StreamWriter> action) {
151+
this.action = action;
152+
}
153+
private readonly Action<StreamWriter> action;
154+
155+
public string Encoding => "shift_jis";
156+
157+
public void Run(string tempFile) {
158+
File.Delete(tempFile);
159+
System.Text.Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
160+
using (var writer = new StreamWriter(tempFile, false, System.Text.Encoding.GetEncoding(Encoding))) {
161+
action.Invoke(writer);
162+
}
163+
}
164+
}
165+
166+
public class ExecuteArgument {
167+
public readonly UProject Project;
168+
public readonly UVoicePart Part;
169+
public readonly UNote First;
170+
public readonly UNote Last;
171+
172+
public ExecuteArgument(UProject project, UVoicePart part, UNote first, UNote last) {
173+
Project = project;
174+
Part = part;
175+
First = first;
176+
Last = last;
177+
}
178+
}
179+
}

OpenUtau/ViewModels/PianoRollViewModel.cs

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Security.Cryptography;
77
using Avalonia.Threading;
88
using DynamicData.Binding;
9+
using OpenUtau.Classic;
910
using OpenUtau.Core;
1011
using OpenUtau.Core.Editing;
1112
using OpenUtau.Core.Ustx;
@@ -112,36 +113,18 @@ public PianoRollViewModel() {
112113
if (NotesViewModel.Part == null || NotesViewModel.Part.notes.Count == 0) {
113114
return;
114115
}
115-
try {
116-
var project = NotesViewModel.Project;
117-
var part = NotesViewModel.Part;
118-
var tempFile = Path.Combine(PathManager.Inst.CachePath, "temp.tmp");
119-
UNote? first = null;
120-
UNote? last = null;
121-
if (NotesViewModel.Selection.IsEmpty) {
122-
first = part.notes.First();
123-
last = part.notes.Last();
124-
} else {
125-
first = NotesViewModel.Selection.FirstOrDefault();
126-
last = NotesViewModel.Selection.LastOrDefault();
127-
}
128-
var sequence = Classic.Ust.WritePlugin(project, part, first, last, tempFile, encoding: plugin.Encoding);
129-
byte[]? beforeHash = HashFile(tempFile);
130-
plugin.Run(tempFile);
131-
byte[]? afterHash = HashFile(tempFile);
132-
if (beforeHash == null || afterHash == null || Enumerable.SequenceEqual(beforeHash, afterHash)) {
133-
Log.Information("Legacy plugin temp file has not changed.");
134-
return;
135-
}
136-
Log.Information("Legacy plugin temp file has changed.");
137-
var (toRemove, toAdd) = Classic.Ust.ParsePlugin(project, part, first, last, sequence, tempFile, encoding: plugin.Encoding);
138-
DocManager.Inst.StartUndoGroup();
139-
DocManager.Inst.ExecuteCmd(new RemoveNoteCommand(part, toRemove));
140-
DocManager.Inst.ExecuteCmd(new AddNoteCommand(part, toAdd));
141-
DocManager.Inst.EndUndoGroup();
142-
} catch (Exception e) {
143-
DocManager.Inst.ExecuteCmd(new ErrorMessageNotification("Failed to execute plugin", e));
116+
var part = NotesViewModel.Part;
117+
UNote? first;
118+
UNote? last;
119+
if (NotesViewModel.Selection.IsEmpty) {
120+
first = part.notes.First();
121+
last = part.notes.Last();
122+
} else {
123+
first = NotesViewModel.Selection.FirstOrDefault();
124+
last = NotesViewModel.Selection.LastOrDefault();
144125
}
126+
var runner = PluginRunner.from(PathManager.Inst, DocManager.Inst);
127+
runner.Execute(NotesViewModel.Project, part, first, last, plugin);
145128
});
146129
LegacyPlugins.AddRange(DocManager.Inst.Plugins.Select(plugin => new MenuItemViewModel() {
147130
Header = plugin.Name,

0 commit comments

Comments
 (0)