1 #include <opustags.h>
2 #include "tap.h"
3
4 #include <string.h>
5
6 using namespace std::literals::string_literals;
7
check_read_comments()8 void check_read_comments()
9 {
10 std::list<std::string> comments;
11 ot::status rc;
12 {
13 std::string txt = "TITLE=a b c\n\nARTIST=X\nArtist=Y\n"s;
14 ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
15 rc = ot::read_comments(input.get(), comments, false);
16 if (rc != ot::st::ok)
17 throw failure("could not read comments");
18 auto&& expected = {"TITLE=a b c", "ARTIST=X", "Artist=Y"};
19 if (!std::equal(comments.begin(), comments.end(), expected.begin(), expected.end()))
20 throw failure("parsed user comments did not match expectations");
21 }
22 {
23 std::string txt = "CORRUPTED=\xFF\xFF\n"s;
24 ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
25 rc = ot::read_comments(input.get(), comments, false);
26 if (rc != ot::st::badly_encoded)
27 throw failure("did not get the expected error reading corrupted data");
28 }
29 {
30 std::string txt = "RAW=\xFF\xFF\n"s;
31 ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
32 rc = ot::read_comments(input.get(), comments, true);
33 if (rc != ot::st::ok)
34 throw failure("could not read comments");
35 if (comments.front() != "RAW=\xFF\xFF")
36 throw failure("parsed user comments did not match expectations");
37 }
38 {
39 std::string txt = "MALFORMED\n"s;
40 ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
41 rc = ot::read_comments(input.get(), comments, false);
42 if (rc != ot::st::error)
43 throw failure("did not get the expected error reading malformed comments");
44 }
45 }
46
47 /**
48 * Wrap #ot::parse_options with a higher-level interface much more convenient for testing.
49 * In practice, the argc/argv combo are enough though for the current state of opustags.
50 */
parse_options(const std::vector<const char * > & args,ot::options & opt,FILE * comments)51 static ot::status parse_options(const std::vector<const char*>& args, ot::options& opt, FILE *comments)
52 {
53 int argc = args.size();
54 char* argv[argc];
55 for (int i = 0; i < argc; ++i)
56 argv[i] = strdup(args[i]);
57 ot::status rc = ot::parse_options(argc, argv, opt, comments);
58 for (int i = 0; i < argc; ++i)
59 free(argv[i]);
60 return rc;
61 }
62
check_good_arguments()63 void check_good_arguments()
64 {
65 auto parse = [](std::vector<const char*> args) {
66 ot::options opt;
67 std::string txt = "N=1\n"s;
68 ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
69 ot::status rc = parse_options(args, opt, input.get());
70 if (rc.code != ot::st::ok)
71 throw failure("unexpected option parsing error");
72 return opt;
73 };
74
75 ot::options opt;
76 opt = parse({"opustags", "--help", "x", "-o", "y"});
77 if (!opt.print_help)
78 throw failure("did not catch --help");
79
80 opt = parse({"opustags", "x", "--output", "y", "-D", "-s", "X=Y Z", "-d", "a=b"});
81 if (opt.paths_in.size() != 1 || opt.paths_in.front() != "x" || !opt.path_out ||
82 opt.path_out != "y" || !opt.delete_all || opt.overwrite || opt.to_delete.size() != 2 ||
83 opt.to_delete.front() != "X" || *std::next(opt.to_delete.begin()) != "a=b" ||
84 opt.to_add != std::list<std::string>{"X=Y Z"})
85 throw failure("unexpected option parsing result for case #1");
86
87 opt = parse({"opustags", "-S", "x", "-S", "-a", "x=y z", "-i"});
88 if (opt.paths_in.size() != 1 || opt.paths_in.front() != "x" || opt.path_out ||
89 !opt.overwrite || opt.to_delete.size() != 0 ||
90 opt.to_add != std::list<std::string>{"N=1", "x=y z"})
91 throw failure("unexpected option parsing result for case #2");
92
93 opt = parse({"opustags", "-i", "x", "y", "z"});
94 if (opt.paths_in.size() != 3 || opt.paths_in[0] != "x" || opt.paths_in[1] != "y" ||
95 opt.paths_in[2] != "z" || !opt.overwrite || !opt.in_place)
96 throw failure("unexpected option parsing result for case #3");
97
98 opt = parse({"opustags", "-ie", "x"});
99 if (opt.paths_in.size() != 1 || opt.paths_in[0] != "x" ||
100 !opt.edit_interactively || !opt.overwrite || !opt.in_place)
101 throw failure("unexpected option parsing result for case #4");
102
103 opt = parse({"opustags", "-a", "X=\xFF", "--raw", "x"});
104 if (!opt.raw || opt.to_add.front() != "X=\xFF")
105 throw failure("--raw did not disable transcoding");
106 }
107
check_bad_arguments()108 void check_bad_arguments()
109 {
110 auto error_code_case = [](std::vector<const char*> args, const char* message, ot::st error_code, const std::string& name) {
111 ot::options opt;
112 std::string txt = "N=1\nINVALID"s;
113 ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
114 ot::status rc = parse_options(args, opt, input.get());
115 if (rc.code != error_code)
116 throw failure("bad error code for case " + name);
117 if (rc.message != message)
118 throw failure("bad error message for case " + name + ", got: " + rc.message);
119 };
120 auto error_case = [&error_code_case](std::vector<const char*> args, const char* message, const std::string& name) {
121 error_code_case(args, message, ot::st::bad_arguments, name);
122 };
123 error_case({"opustags"}, "No arguments specified. Use -h for help.", "no arguments");
124 error_case({"opustags", "-a", "X"}, "Comment does not contain an equal sign: X.", "bad comment for -a");
125 error_case({"opustags", "--set", "X"}, "Comment does not contain an equal sign: X.", "bad comment for --set");
126 error_case({"opustags", "-a"}, "Missing value for option '-a'.", "short option with missing value");
127 error_case({"opustags", "--add"}, "Missing value for option '--add'.", "long option with missing value");
128 error_case({"opustags", "-x"}, "Unrecognized option '-x'.", "unrecognized short option");
129 error_case({"opustags", "--derp"}, "Unrecognized option '--derp'.", "unrecognized long option");
130 error_case({"opustags", "-x=y"}, "Unrecognized option '-x'.", "unrecognized short option with value");
131 error_case({"opustags", "--derp=y"}, "Unrecognized option '--derp=y'.", "unrecognized long option with value");
132 error_case({"opustags", "-aX=Y"}, "Exactly one input file must be specified.", "no input file");
133 error_case({"opustags", "-i", "-o", "/dev/null", "-"}, "Cannot combine --in-place and --output.", "in-place + output");
134 error_case({"opustags", "-S", "-"}, "Cannot use standard input as input file when --set-all is specified.",
135 "set all and read opus from stdin");
136 error_case({"opustags", "-i", "-"}, "Cannot modify standard input in place.", "write stdin in-place");
137 error_case({"opustags", "-o", "x", "--output", "y", "z"},
138 "Cannot specify --output more than once.", "double output");
139 error_code_case({"opustags", "-S", "x"}, "Malformed tag: INVALID", ot::st::error, "attempt to read invalid argument with -S");
140 error_case({"opustags", "-o", "", "--output", "y", "z"},
141 "Cannot specify --output more than once.", "double output with first filename empty");
142 error_case({"opustags", "-e", "-i", "x", "y"},
143 "Exactly one input file must be specified.", "editing interactively two files at once");
144 error_case({"opustags", "--edit", "-", "-o", "x"},
145 "Cannot edit interactively when standard input or standard output are already used.",
146 "editing interactively from stdandard intput");
147 error_case({"opustags", "--edit", "x", "-o", "-"},
148 "Cannot edit interactively when standard input or standard output are already used.",
149 "editing interactively to stdandard output");
150 error_case({"opustags", "--edit", "x"}, "Cannot edit interactively when no output is specified.", "editing without output");
151 error_case({"opustags", "--edit", "x", "-i", "-a", "X=Y"}, "Cannot mix --edit with -adDsS.", "mixing -e and -a");
152 error_case({"opustags", "--edit", "x", "-i", "-d", "X"}, "Cannot mix --edit with -adDsS.", "mixing -e and -d");
153 error_case({"opustags", "--edit", "x", "-i", "-D"}, "Cannot mix --edit with -adDsS.", "mixing -e and -D");
154 error_case({"opustags", "--edit", "x", "-i", "-S"}, "Cannot mix --edit with -adDsS.", "mixing -e and -S");
155 error_case({"opustags", "-d", "\xFF", "x"},
156 "Could not encode argument into UTF-8: Invalid or incomplete multibyte or wide character.",
157 "-d with binary data");
158 error_case({"opustags", "-a", "X=\xFF", "x"},
159 "Could not encode argument into UTF-8: Invalid or incomplete multibyte or wide character.",
160 "-a with binary data");
161 error_case({"opustags", "-s", "X=\xFF", "x"},
162 "Could not encode argument into UTF-8: Invalid or incomplete multibyte or wide character.",
163 "-s with binary data");
164 }
165
check_delete_comments()166 static void check_delete_comments()
167 {
168 using C = std::list<std::string>;
169 C original = {"TITLE=X", "Title=Y", "Title=Z", "ARTIST=A", "artIst=B"};
170
171 C edited = original;
172 ot::delete_comments(edited, "derp");
173 if (!std::equal(edited.begin(), edited.end(), original.begin(), original.end()))
174 throw failure("should not have deleted anything");
175
176 ot::delete_comments(edited, "Title");
177 C expected = {"ARTIST=A", "artIst=B"};
178 if (!std::equal(edited.begin(), edited.end(), expected.begin(), expected.end()))
179 throw failure("did not delete all titles correctly");
180
181 edited = original;
182 ot::delete_comments(edited, "titlE=Y");
183 ot::delete_comments(edited, "Title=z");
184 expected = {"TITLE=X", "Title=Z", "ARTIST=A", "artIst=B"};
185 if (!std::equal(edited.begin(), edited.end(), expected.begin(), expected.end()))
186 throw failure("did not delete a specific title correctly");
187 }
188
main(int argc,char ** argv)189 int main(int argc, char **argv)
190 {
191 std::cout << "1..4\n";
192 run(check_read_comments, "check tags parsing");
193 run(check_good_arguments, "check options parsing");
194 run(check_bad_arguments, "check options parsing errors");
195 run(check_delete_comments, "delete comments");
196 return 0;
197 }
198