App-FuguVM

 view release on metacpan or  search on metacpan

t/fuguvm/cli.t  view on Meta::CPAN

    my $left = $cache->list;
    is(scalar @$left, 1, '--stale keeps exactly one entry');
    is($left->[0]{key}, $current, 'and it is the invoked VM\'s key');
    ok(!-e $cache->installed_dir . '/.tmp.999.abcdef',
	'--stale also sweeps interrupted store trees');

    is(App::FuguVM::CLI->run("--project=$project", '--quiet', 'cache', 'clear'),
	0, 'bare cache clear succeeds');
    is(scalar @{ $cache->list }, 0, 'bare clear removes everything');
}

# The proxy's downloads share cache_dir with the images, and nothing
# else bounds them. Thus the same command prunes both. --stale keeps the
# OpenBSD version the invoked VM installs. A bare clear keeps nothing.
{
    my $project = _cache_project();
    my $proxy = App::FuguVM::Proxy::Cache->new("$project/cache");

    _fake_download($project, '7.8/arm64/base78.tgz');
    _fake_download($project, '7.7/arm64/base77.tgz');
    is(scalar @{ $proxy->list }, 2, 'two cached downloads before pruning');

    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'cache', 'clear', '--stale'),
	0, 'cache clear --stale succeeds');

    is_deeply([map { $_->{url} } @{ $proxy->list }],
	['http://cdn.openbsd.org/pub/OpenBSD/7.8/arm64/base78.tgz'],
	'--stale keeps the version the configured VM installs');

    is(App::FuguVM::CLI->run("--project=$project", '--quiet', 'cache', 'clear'),
	0, 'bare cache clear succeeds');
    is(scalar @{ $proxy->list }, 0, 'bare clear empties the proxy too');
}

# The listing also reports the proxy. Thus a user who decides on a
# prune sees both halves of cache_dir.
{
    my $project = _cache_project();
    _fake_download($project, '7.7/arm64/base77.tgz');

    my $err = _capture_stderr($project, 'cache', 'list');
    like($err, qr/Proxy downloads/, 'cache list reports the proxy store');
    like($err, qr/OpenBSD 7\.7/, 'broken down by version');
    like($err, qr/No cached images/,
	'and still says the images are empty');
}

# clear refuses while a VM runs on a disk that the entry backs
SKIP: {
    my $has_qemu = `which qemu-img 2>/dev/null`;
    skip 'qemu-img not installed', 4 unless $has_qemu;

    my $project = _cache_project();
    my $cache = App::FuguVM::DiskCache->new("$project/cache");
    my $key = '7.8-arm64-cafebabe';

    my $source = "$project/source.qcow2";
    system('qemu-img', 'create', '-f', 'qcow2', $source, '16M') == 0
	or skip 'cannot create a test disk image', 4;
    my $base = $cache->store($key, $source, { root_password => 'pw' });

    # A working disk in this checkout, backed by the cached entry
    my $state_dir = "$project/.fuguvm/state";
    App::FuguVM::Disk->new($state_dir)->create('default', undef, $base);

    # This test process stands in for a live QEMU
    make_path("$state_dir/default");
    open my $pidfh, '>', "$state_dir/default/vm.pid" or die $!;
    print $pidfh "$$\n";
    close $pidfh;

    local $SIG{__WARN__} = sub {};
    is(App::FuguVM::CLI->run("--project=$project", '--quiet', 'cache', 'clear'),
	5, 'clear refuses with EXIT_VM_RUNNING while the VM runs');
    ok(defined $cache->lookup($key), 'the entry survives the refusal');

    # Once the VM stops, removal proceeds. A warning reports the orphan.
    unlink "$state_dir/default/vm.pid";
    is(App::FuguVM::CLI->run("--project=$project", '--quiet', 'cache', 'clear'),
	0, 'clear proceeds once the VM is stopped');
    is($cache->lookup($key), undef, 'the entry is gone');
}

# ============================================================
# Snapshot subcommand
# ============================================================

# Usage and name validation
{
    my $project = _cache_project();

    local $SIG{__WARN__} = sub {};
    is(App::FuguVM::CLI->run("--project=$project", 'snapshot'), 2,
	'snapshot without an action returns EXIT_INVALID_ARGS');
    is(App::FuguVM::CLI->run("--project=$project", 'snapshot', 'frobnicate'), 2,
	'unknown snapshot action returns EXIT_INVALID_ARGS');
    is(App::FuguVM::CLI->run("--project=$project", 'snapshot', 'save'), 2,
	'snapshot save without a name returns EXIT_INVALID_ARGS');
    is(App::FuguVM::CLI->run("--project=$project", 'snapshot', 'save', '../x'),
	2, 'a name with a path separator is rejected');
}

# Missing snapshots report a distinct, scriptable exit code. Thus
# callers can do 'snapshot restore || provision-from-scratch'.
{
    my $project = _cache_project();
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'restore', 'deps'),
	11, 'restoring a missing snapshot returns EXIT_SNAPSHOT_NOT_FOUND');
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'rm', 'deps'),
	11, 'removing a missing snapshot returns the same code');
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'list'),
	0, 'listing with nothing cached still succeeds');
}

# Save, restore, and the refusals, over a real backing chain
SKIP: {
    my $has_qemu = `which qemu-img 2>/dev/null`;
    skip 'qemu-img not installed', 12 unless $has_qemu;

    my $project = _cache_project();
    my $state_dir = "$project/.fuguvm/state";
    my $cache = App::FuguVM::DiskCache->new("$project/cache");
    my $key = $cache->key(App::FuguVM::Config->new($project)->load_vm('default'));

    my $source = "$project/source.qcow2";
    system('qemu-img', 'create', '-f', 'qcow2', $source, '16M') == 0
	or skip 'cannot create a test disk image', 12;

    # A snapshot of a standalone disk is not possible. Say so. Do
    # not crash.
    my $disk = App::FuguVM::Disk->new($state_dir);
    $disk->create('default', '16M');
    my $state = App::FuguVM::State->new($state_dir, 'default');
    $state->mark_installed;

    local $SIG{__WARN__} = sub {};
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'save', 'deps'),
	1, 'a standalone disk is a diagnosed error, not a crash');

    # Rebuild the disk as an overlay on a cached base
    my $base = $cache->store($key, $source, { root_password => 'pw' });
    unlink $disk->path('default');
    $disk->create('default', undef, $base);

    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'save', 'deps'),
	0, 'save succeeds on a disk backed by a cached image');
    ok(defined $cache->snapshot_lookup($key, 'deps'), 'the snapshot exists');

    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'list'),
	0, 'list succeeds');

    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'restore', 'deps'),
	0, 'restore succeeds');
    is($disk->backing_file('default'),
	$cache->snapshot_path($key, 'deps'),
	'restore actually replaced the disk with an overlay on the snapshot');

    # Restore from nothing. No disk and no state exist, as in a
    # fresh checkout.
    unlink $disk->path('default');
    unlink "$state_dir/default/status";
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'restore', 'deps'),
	0, 'restore works with no disk and no state');

    my $reseeded = App::FuguVM::State->new($state_dir, 'default');
    ok($reseeded->is_installed, 'restore reseeds installed state');
    is($reseeded->get_root_password, 'pw',
	'restore reseeds the root password from the base');

    # A running VM refuses both save and restore
    open my $pidfh, '>', "$state_dir/default/vm.pid" or die $!;
    print $pidfh "$$\n";
    close $pidfh;

    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'save', 'deps'),
	5, 'save refuses while the VM is running');
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'restore', 'deps'),
	5, 'restore refuses while the VM is running');
    unlink "$state_dir/default/vm.pid";

    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'rm', 'deps'),
	0, 'rm succeeds');
}

# --names is the scriptable listing. It writes bare names on stdout,
# where a shell can read them. It does not go through the stderr logger.
SKIP: {
    my $has_qemu = `which qemu-img 2>/dev/null`;
    skip 'qemu-img not installed', 4 unless $has_qemu;

    my $project = _cache_project();
    my $cache = App::FuguVM::DiskCache->new("$project/cache");
    my $key = $cache->key(App::FuguVM::Config->new($project)->load_vm('default'));

    my $source = "$project/source.qcow2";
    system('qemu-img', 'create', '-f', 'qcow2', $source, '16M') == 0
	or skip 'cannot create a test disk image', 4;
    my $base = $cache->store($key, $source, { root_password => 'pw' });
    App::FuguVM::Disk->new("$project/.fuguvm/state")
	->create('default', undef, $base);
    App::FuguVM::State->new("$project/.fuguvm/state", 'default')->mark_installed;

    is(_capture_stdout($project, 'snapshot', 'list', '--names'), '',
	'nothing on stdout when there are no snapshots');

    App::FuguVM::CLI->run("--project=$project", '--quiet',
	'snapshot', 'save', 'deps-aaa');
    App::FuguVM::CLI->run("--project=$project", '--quiet',
	'snapshot', 'save', 'deps-bbb');

    is(_capture_stdout($project, 'snapshot', 'list', '--names'),
	"deps-aaa\ndeps-bbb\n", 'one bare name per line, sorted');
    is(_capture_stdout($project, 'snapshot', 'list'), '',
	'the human listing writes nothing to stdout');

    local $SIG{__WARN__} = sub {};
    is(App::FuguVM::CLI->run("--project=$project", '--quiet',
	    'snapshot', 'list', '--bogus'),
	2, 'an unknown list option returns EXIT_INVALID_ARGS');
}

# A byte count for a person to read is presentation, so the CLI owns
# it. The cases came with the function from Fugu::Timeout.
subtest '_format_size' => sub {
	my $f = \&App::FuguVM::CLI::_format_size;

	is( $f->(0),       '0B',    'zero' );
	is( $f->(512),     '512B',  'bytes' );
	is( $f->(1023),    '1023B', 'just under 1K' );
	is( $f->(1024),    '1.0K',  'one kilobyte' );
	is( $f->(1536),    '1.5K',  'one and a half' );
	is( $f->(1024**2), '1.0M',  'one megabyte' );
	is( $f->(1024**3), '1.0G',  'one gigabyte' );
	is( $f->(1024**4), '1.0T',  'one terabyte' );
	is( $f->(1024**5), '1024.0T',
		'past the largest unit it keeps counting' );
	is( $f->(undef), '?',
		'a size nobody could measure is not a size of zero' );
};

done_testing();

# A project whose cache_dir points inside the project. Thus the tests
# never touch the developer's real ~/.cache/fuguvm.
sub _cache_project
{
    my $project = tempdir(CLEANUP => 1);
    make_path("$project/.fuguvm/vms", "$project/.fuguvm/state");

    open my $fh, '>', "$project/.fuguvmrc" or die $!;
    print $fh "cache_dir $project/cache\n";
    print $fh "state_dir .fuguvm/state\n";
    print $fh "default_vm default\n";
    print $fh "vm \"default\" {\n";
    print $fh "\tversion 7.8\n";
    print $fh "\tdisk_size 8G\n";
    print $fh "}\n";
    close $fh;

t/fuguvm/cli.t  view on Meta::CPAN

    open my $saved, '>&', \*STDOUT or die $!;
    close STDOUT;
    open STDOUT, '>', \$out or die $!;

    App::FuguVM::CLI->run("--project=$project", '--quiet', @args);

    close STDOUT;
    open STDOUT, '>&', $saved or die $!;
    close $saved;

    return $out;
}

# The logger writes to stderr. Thus this helper captures the human
# listing apart from the scriptable output above.
sub _capture_stderr
{
    my ($project, @args) = @_;
    my $err = '';

    open my $saved, '>&', \*STDERR or die $!;
    close STDERR;
    open STDERR, '>', \$err or die $!;

    App::FuguVM::CLI->run("--project=$project", @args);

    close STDERR;
    open STDERR, '>&', $saved or die $!;
    close $saved;

    return $err;
}

# A cached proxy download, seeded on disk and not through store().
# store() uses cache_path(), which wants URI, a develop dependency.
sub _fake_download
{
    my ($project, $rel) = @_;
    my $dir = "$project/cache/proxy/cdn.openbsd.org/pub/OpenBSD/$rel";

    $dir =~ m{\A(.*)/} and make_path($1);
    open my $fh, '>', $dir or die $!;
    print $fh 'not a real file set';
    close $fh;

    return $dir;
}

# A complete-looking cache entry without the cost of a real image
sub _fake_entry
{
    my ($cache, $key) = @_;
    my $dir = $cache->entry_dir($key);
    make_path($dir);

    open my $bh, '>', "$dir/base.qcow2" or die $!;
    print $bh 'not a real image';
    close $bh;

    open my $mh, '>', "$dir/meta.json" or die $!;
    print $mh qq({"key":"$key","created_at":1,"root_password":"pw"});
    close $mh;

    return $dir;
}



( run in 1.851 second using v1.01-cache-2.11-cpan-4ef0a570458 )