Added start of testing framework.
This commit is contained in:
parent
84bfbb9f9f
commit
f6d850f4e6
131
source/app.d
131
source/app.d
|
@ -1,65 +1,84 @@
|
|||
import std.typecons;
|
||||
import std.path;
|
||||
import std.file;
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
import std.algorithm;
|
||||
import std.getopt;
|
||||
import filesizes;
|
||||
import progress;
|
||||
|
||||
import utils;
|
||||
import ingest;
|
||||
|
||||
const DEFAULT_MEDIA_DIR = "/media";
|
||||
const DEFAULT_OUTPUT_DIR = "raw";
|
||||
|
||||
int main(string[] args) {
|
||||
writeln(
|
||||
"+---------------------------------+\n" ~
|
||||
"| |\n" ~
|
||||
"| GoPro Ingester |\n" ~
|
||||
"| v1.0.0 |\n" ~
|
||||
"| by Andrew Lalis |\n" ~
|
||||
"| |\n" ~
|
||||
"+---------------------------------+\n"
|
||||
);
|
||||
IngestConfig config;
|
||||
config.outputDir = buildPath(getcwd(), DEFAULT_OUTPUT_DIR);
|
||||
string mediaSearchDir = DEFAULT_MEDIA_DIR;
|
||||
auto helpInfo = getopt(
|
||||
args,
|
||||
"mediaDir|i",
|
||||
format!"The base directory from which to search for the GoPro media. Defaults to \"%s\"."(mediaSearchDir),
|
||||
&mediaSearchDir,
|
||||
"outputDir|o",
|
||||
format!"The directory to copy data to. Defaults to \"%s\". Will create the directory if it doesn't exist yet."(config.outputDir),
|
||||
&config.outputDir,
|
||||
"force|f",
|
||||
format!"Whether to forcibly overwrite existing files. Defaults to %s."(config.force),
|
||||
&config.force,
|
||||
"dryRun|d",
|
||||
format!"Whether to perform a dry-run (don't actually copy anything). Defaults to %s."(config.dryRun),
|
||||
&config.dryRun,
|
||||
"bufferSize|b",
|
||||
format!"The size of the buffer for copying files, in bytes. Defaults to %s."(formatFilesize(config.bufferSize)),
|
||||
&config.bufferSize,
|
||||
"clean|c",
|
||||
format!"Whether to remove files from the GoPro media card after copying. Defaults to %s."(config.clean),
|
||||
&config.clean
|
||||
);
|
||||
|
||||
if (helpInfo.helpWanted) {
|
||||
defaultGetoptPrinter("Ingestion tool for importing data from GoPro media cards.", helpInfo.options);
|
||||
printBanner();
|
||||
CliResult result = parseArgs(args);
|
||||
if (result.type == CliResultType.NO_CONTENT) {
|
||||
return 0;
|
||||
} else if (result.type == CliResultType.MISSING_MEDIA) {
|
||||
return 1;
|
||||
} else {
|
||||
return copyFiles(result.config);
|
||||
}
|
||||
}
|
||||
|
||||
auto nullableGoProDir = getGoProDir(mediaSearchDir);
|
||||
if (nullableGoProDir.isNull) {
|
||||
writeln("Couldn't find GoPro directory.");
|
||||
return 1;
|
||||
unittest {
|
||||
// First some utilities to make the tests simpler.
|
||||
import std.stdio;
|
||||
import std.file;
|
||||
import std.path;
|
||||
import std.algorithm;
|
||||
import std.string;
|
||||
import utils;
|
||||
|
||||
struct DirView {
|
||||
DirEntry[] entries;
|
||||
}
|
||||
config.inputDir = nullableGoProDir.get();
|
||||
writefln!"Found GoPro media at %s."(config.inputDir);
|
||||
return copyFiles(config);
|
||||
|
||||
DirView getDirView(string dir) {
|
||||
DirView view;
|
||||
foreach (DirEntry entry; dirEntries(dir, SpanMode.shallow)) {
|
||||
view.entries ~= entry;
|
||||
}
|
||||
view.entries.sort!((a, b) => a.name > b.name);
|
||||
return view;
|
||||
}
|
||||
|
||||
int callApp(string[] args ...) {
|
||||
return main(["app"] ~ args);
|
||||
}
|
||||
|
||||
string getBaseCardDir(string name) {
|
||||
return buildPath("test", "media-cards", "card-" ~ name);
|
||||
}
|
||||
|
||||
string getTestCardDir(string name) {
|
||||
return buildPath("test", "media-cards", "card-test-" ~ name);
|
||||
}
|
||||
|
||||
void assertCardsUnchanged(string[] cards ...) {
|
||||
foreach (string card; cards) {
|
||||
string baseDir = getBaseCardDir(card);
|
||||
string testDir = getTestCardDir(card);
|
||||
if (exists(testDir)) {
|
||||
assert(getDirView(baseDir) == getDirView(testDir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void prepareCardTests(string[] cards ...) {
|
||||
foreach (string card; cards) {
|
||||
copyDir(getBaseCardDir(card), getTestCardDir(card));
|
||||
}
|
||||
}
|
||||
|
||||
void cleanupCardTests(string[] cards ...) {
|
||||
foreach (string card; cards) {
|
||||
rmdirRecurse(getTestCardDir(card));
|
||||
}
|
||||
}
|
||||
|
||||
string[] allCards = ["1", "2", "3"];
|
||||
prepareCardTests(allCards);
|
||||
writeln(isDir(getBaseCardDir("1")));
|
||||
assert(callApp("-h") == 0);
|
||||
assertCardsUnchanged(allCards);
|
||||
assert(callApp("--help") == 0);
|
||||
assertCardsUnchanged(allCards);
|
||||
assert(callApp("-f", "-h") == 0);
|
||||
assertCardsUnchanged(allCards);
|
||||
// Ingesting from an empty card shouldn't have any effect.
|
||||
assert(callApp("-i", getTestCardDir("1")) == 0);
|
||||
assertCardsUnchanged("1");
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import std.string;
|
|||
import std.typecons;
|
||||
import filesizes;
|
||||
import progress;
|
||||
import utils;
|
||||
|
||||
const DEFAULT_BUFFER_SIZE = 1024 * 1024;
|
||||
|
||||
|
@ -43,7 +44,7 @@ public struct IngestConfig {
|
|||
*/
|
||||
private bool shouldCopyFile(DirEntry entry, string targetFile, bool force) {
|
||||
return entry.isFile() &&
|
||||
(entry.name.endsWith(".MP4") || entry.name.endsWith(".WAV")) &&
|
||||
(endsWithAny(entry.name, ".MP4", ".mp4", ".WAV", ".wav")) &&
|
||||
(!exists(targetFile) || getSize(targetFile) != entry.size || force);
|
||||
}
|
||||
|
||||
|
@ -137,3 +138,17 @@ public int copyFiles(IngestConfig config) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
unittest {
|
||||
import testutils;
|
||||
|
||||
// Test a dry run.
|
||||
prepareCardTests("1");
|
||||
IngestConfig c1;
|
||||
c1.dryRun = true;
|
||||
c1.inputDir = getTestCardDir("1");
|
||||
c1.outputDir = "test-out";
|
||||
assert(copyFiles(c1) == 0);
|
||||
assertCardsUnchanged("1");
|
||||
cleanupCardTests("1");
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
module testutils;
|
||||
|
||||
import std.stdio;
|
||||
import std.file;
|
||||
import std.path;
|
||||
import std.algorithm;
|
||||
import std.string;
|
||||
import utils;
|
||||
|
||||
struct DirView {
|
||||
DirEntry[] entries;
|
||||
}
|
||||
|
||||
DirView getDirView(string dir) {
|
||||
DirView view;
|
||||
foreach (DirEntry entry; dirEntries(dir, SpanMode.shallow)) {
|
||||
view.entries ~= entry;
|
||||
}
|
||||
view.entries.sort!((a, b) => a.name > b.name);
|
||||
return view;
|
||||
}
|
||||
|
||||
string getBaseCardDir(string name) {
|
||||
return buildPath("test", "media-cards", "card-" ~ name);
|
||||
}
|
||||
|
||||
string getTestCardDir(string name) {
|
||||
return buildPath("test", "media-cards", "card-test-" ~ name);
|
||||
}
|
||||
|
||||
void assertCardsUnchanged(string[] cards ...) {
|
||||
foreach (string card; cards) {
|
||||
string baseDir = getBaseCardDir(card);
|
||||
string testDir = getTestCardDir(card);
|
||||
if (exists(testDir)) {
|
||||
assert(getDirView(baseDir) == getDirView(testDir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void prepareCardTests(string[] cards ...) {
|
||||
foreach (string card; cards) {
|
||||
copyDir(getBaseCardDir(card), getTestCardDir(card));
|
||||
}
|
||||
}
|
||||
|
||||
void cleanupCardTests(string[] cards ...) {
|
||||
foreach (string card; cards) {
|
||||
rmdirRecurse(getTestCardDir(card));
|
||||
}
|
||||
}
|
127
source/utils.d
127
source/utils.d
|
@ -1,8 +1,129 @@
|
|||
module utils;
|
||||
|
||||
import std.typecons;
|
||||
import std.file;
|
||||
import ingest;
|
||||
|
||||
const DEFAULT_MEDIA_DIR = "/media";
|
||||
const DEFAULT_OUTPUT_DIR = "raw";
|
||||
|
||||
public enum CliResultType {
|
||||
OK,
|
||||
NO_CONTENT,
|
||||
MISSING_MEDIA
|
||||
}
|
||||
|
||||
public struct CliResult {
|
||||
CliResultType type;
|
||||
IngestConfig config;
|
||||
}
|
||||
|
||||
public CliResult parseArgs(string[] args) {
|
||||
import std.path;
|
||||
import std.file;
|
||||
import std.getopt;
|
||||
import std.string;
|
||||
import std.stdio;
|
||||
import filesizes;
|
||||
CliResult result;
|
||||
IngestConfig config;
|
||||
config.outputDir = buildPath(getcwd(), DEFAULT_OUTPUT_DIR);
|
||||
string mediaSearchDir = DEFAULT_MEDIA_DIR;
|
||||
auto helpInfo = getopt(
|
||||
args,
|
||||
"mediaDir|i",
|
||||
format!"The base directory from which to search for the GoPro media. Defaults to \"%s\"."(mediaSearchDir),
|
||||
&mediaSearchDir,
|
||||
"outputDir|o",
|
||||
format!"The directory to copy data to. Defaults to \"%s\". Will create the directory if it doesn't exist yet."(config.outputDir),
|
||||
&config.outputDir,
|
||||
"force|f",
|
||||
format!"Whether to forcibly overwrite existing files. Defaults to %s."(config.force),
|
||||
&config.force,
|
||||
"dryRun|d",
|
||||
format!"Whether to perform a dry-run (don't actually copy anything). Defaults to %s."(config.dryRun),
|
||||
&config.dryRun,
|
||||
"bufferSize|b",
|
||||
format!"The size of the buffer for copying files, in bytes. Defaults to %s."(formatFilesize(config.bufferSize)),
|
||||
&config.bufferSize,
|
||||
"clean|c",
|
||||
format!"Whether to remove files from the GoPro media card after copying. Defaults to %s."(config.clean),
|
||||
&config.clean
|
||||
);
|
||||
|
||||
if (helpInfo.helpWanted) {
|
||||
defaultGetoptPrinter("Ingestion tool for importing data from GoPro media cards.", helpInfo.options);
|
||||
result.type = CliResultType.NO_CONTENT;
|
||||
return result;
|
||||
} else {
|
||||
auto nullableGoProDir = getGoProDir(mediaSearchDir);
|
||||
if (nullableGoProDir.isNull) {
|
||||
writeln("Couldn't find GoPro directory.");
|
||||
result.type = CliResultType.MISSING_MEDIA;
|
||||
} else {
|
||||
config.inputDir = nullableGoProDir.get();
|
||||
writefln!"Found GoPro media at %s."(config.inputDir);
|
||||
result.type = CliResultType.OK;
|
||||
result.config = config;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert(parseArgs(["app", "-h"]).type == CliResultType.NO_CONTENT);
|
||||
// TODO: Expand tests.
|
||||
}
|
||||
|
||||
public void printBanner() {
|
||||
import std.stdio : writeln;
|
||||
writeln(
|
||||
"+---------------------------------+\n" ~
|
||||
"| |\n" ~
|
||||
"| GoPro Ingester |\n" ~
|
||||
"| v1.0.0 |\n" ~
|
||||
"| by Andrew Lalis |\n" ~
|
||||
"| |\n" ~
|
||||
"+---------------------------------+\n"
|
||||
);
|
||||
}
|
||||
|
||||
public bool endsWithAny(string s, string[] suffixes ...) {
|
||||
import std.algorithm : endsWith;
|
||||
foreach (string suffix; suffixes) {
|
||||
if (s.endsWith(suffix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert(endsWithAny("abc", "a", "b", "c"));
|
||||
assert(!endsWithAny("abc", "d"));
|
||||
assert(!endsWithAny("abc", "A", "B", "C"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all files from the given source directory, to the given destination
|
||||
* directory. Will create the destination directory if it doesn't exist yet.
|
||||
* Overwrites any files that already exist in the destination directory.
|
||||
* Params:
|
||||
* sourceDir = The source directory to copy from.
|
||||
* destDir = The destination directory to copy to.
|
||||
*/
|
||||
public void copyDir(string sourceDir, string destDir) {
|
||||
import std.file;
|
||||
if (!isDir(sourceDir)) return;
|
||||
if (exists(destDir) && !isDir(destDir)) return;
|
||||
if (!exists(destDir)) mkdirRecurse(destDir);
|
||||
import std.path : buildPath, baseName;
|
||||
foreach (DirEntry entry; dirEntries(sourceDir, SpanMode.shallow)) {
|
||||
string destPath = buildPath(destDir, baseName(entry.name));
|
||||
if (entry.isDir) {
|
||||
copyDir(entry.name, destPath);
|
||||
} else if (entry.isFile) {
|
||||
copy(entry.name, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a GoPro's media directory.
|
||||
|
@ -11,7 +132,9 @@ import std.path;
|
|||
* Returns: A nullable string that, if present, refers to the GoPro's media
|
||||
* directory.
|
||||
*/
|
||||
public Nullable!string getGoProDir(string baseDir) {
|
||||
private Nullable!string getGoProDir(string baseDir) {
|
||||
import std.file;
|
||||
import std.path;
|
||||
if (!exists(baseDir) || !isDir(baseDir)) return Nullable!string.init;
|
||||
foreach (dir; std.file.dirEntries(baseDir, SpanMode.breadth)) {
|
||||
// We know that a GoPro contains DCIM/100GOPRO in it.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Hello
|
|
@ -0,0 +1,2 @@
|
|||
# Card 1
|
||||
This is an empty card.
|
|
@ -0,0 +1 @@
|
|||
Hello
|
|
@ -0,0 +1 @@
|
|||
# Card 2
|
|
@ -0,0 +1 @@
|
|||
Hello
|
|
@ -0,0 +1 @@
|
|||
# Card 3
|
Loading…
Reference in New Issue