File Coverage

lib/NBI/Manifest.pm
Criterion Covered Total %
statement 78 80 97.5
branch 12 18 66.6
condition 28 45 62.2
subroutine 11 11 100.0
pod 5 5 100.0
total 134 159 84.2


line stmt bran cond sub pod time code
1             package NBI::Manifest;
2             #ABSTRACT: Provenance record for a single nbilaunch job run
3             #
4             # NBI::Manifest - Reads and writes JSON provenance records for nbilaunch.
5             #
6             # DESCRIPTION:
7             # Serialises every input, parameter, output, and Slurm resource used by a
8             # single launcher invocation to a JSON file that lives alongside the results.
9             # Written in two phases:
10             # 1. At submission (by nbilaunch): status "submitted", no job ID yet.
11             # 2. At job end (by injected shell): status "success" or "failure",
12             # exit code, completion time, and checksums are patched in.
13             #
14             # The on-disk format uses JSON::PP with canonical key ordering so that
15             # diffs between runs are clean.
16             #
17             # RELATIONSHIPS:
18             # - Created by NBI::Launcher->build() and written by bin/nbilaunch.
19             # - The injected shell function _nbi_manifest_update patches the file
20             # in-place using a perl one-liner (no jq dependency).
21             # - Complex launchers can chain jobs via NBI::Manifest->load() to read
22             # a previous run's output paths.
23             #
24              
25 2     2   305794 use 5.012;
  2         6  
26 2     2   10 use strict;
  2         3  
  2         44  
27 2     2   6 use warnings;
  2         3  
  2         106  
28 2     2   12 use Carp qw(confess);
  2         3  
  2         104  
29 2     2   8 use JSON::PP;
  2         2  
  2         172  
30 2     2   444 use POSIX qw(strftime);
  2         5830  
  2         25  
31              
32             $NBI::Manifest::VERSION = $NBI::Slurm::VERSION;
33              
34             # Fields serialised to JSON (in this order, for readability).
35             # The internal _path field is excluded.
36             my @JSON_FIELDS = qw(
37             tool tool_version launcher_version nbi_slurm_version
38             submitted_at completed_at
39             slurm_job_id slurm_queue slurm_cpus slurm_mem_gb
40             host user status exit_code sample
41             inputs params outputs
42             outdir scratch checksums script
43             );
44              
45             sub new {
46 5     5 1 3508 my ($class, %args) = @_;
47              
48 5         14 my $self = bless {}, $class;
49              
50             # Required fields
51 5         11 for my $f (qw(tool sample outdir)) {
52             confess "ERROR NBI::Manifest: missing required field '$f'\n"
53 14 100       381 unless defined $args{$f};
54             }
55              
56             # Populate all known fields with defaults where sensible
57 3         11 $self->{tool} = $args{tool};
58 3   100     11 $self->{tool_version} = $args{tool_version} // 'unknown';
59 3   50     16 $self->{launcher_version} = $args{launcher_version} // '0.1.0';
60 3   50     12 $self->{nbi_slurm_version} = $args{nbi_slurm_version} // ($NBI::Slurm::VERSION // 'unknown');
      33        
61             $self->{submitted_at} = $args{submitted_at}
62 3   33     96 // strftime("%Y-%m-%dT%H:%M:%SZ", gmtime());
63 3         9 $self->{completed_at} = $args{completed_at}; # undef until job ends
64 3         5 $self->{slurm_job_id} = $args{slurm_job_id}; # undef until submitted
65 3   100     12 $self->{slurm_queue} = $args{slurm_queue} // 'unknown';
66 3   100     10 $self->{slurm_cpus} = $args{slurm_cpus} // 1;
67 3   100     7 $self->{slurm_mem_gb} = $args{slurm_mem_gb} // 0;
68             $self->{host} = $args{host}
69             // $ENV{HOSTNAME}
70 3   33     22 // do { chomp(my $h = `hostname 2>/dev/null`); $h } // 'unknown';
  0   33     0  
  0   0     0  
71 3   33     15 $self->{user} = $args{user} // $ENV{USER} // 'unknown';
      50        
72 3   100     12 $self->{status} = $args{status} // 'submitted';
73 3         6 $self->{exit_code} = $args{exit_code}; # undef until job ends
74 3         8 $self->{sample} = $args{sample};
75 3   100     8 $self->{inputs} = $args{inputs} // {};
76 3   100     6 $self->{params} = $args{params} // {};
77 3   100     10 $self->{outputs} = $args{outputs} // {};
78 3         5 $self->{outdir} = $args{outdir};
79 3   50     13 $self->{scratch} = $args{scratch} // '';
80 3   50     21 $self->{checksums} = $args{checksums} // {};
81 3   100     15 $self->{script} = $args{script} // '';
82              
83             # Internal: path where this manifest lives on disk (not serialised)
84 3         4 $self->{_path} = $args{_path};
85              
86 3         14 return $self;
87             }
88              
89             # ── write($path) ──────────────────────────────────────────────────────────────
90             # Serialise to JSON and write to $path. Records _path for future update() calls.
91             sub write {
92 3     3 1 3468 my ($self, $path) = @_;
93 3 50       7 confess "ERROR NBI::Manifest::write: path required\n" unless defined $path;
94              
95             # Build the hash to serialise (JSON_FIELDS order, excluding _path)
96 3         4 my %data;
97 3         7 for my $f (@JSON_FIELDS) {
98 66         107 $data{$f} = $self->{$f};
99             }
100              
101 3         16 my $json = JSON::PP->new->utf8->pretty->canonical->encode(\%data);
102              
103 3 50       3090 open(my $fh, '>', $path)
104             or confess "ERROR NBI::Manifest::write: cannot open '$path': $!\n";
105 3         61 print $fh $json;
106 3         319 close $fh;
107              
108 3         14 $self->{_path} = $path;
109 3         28 return $self;
110             }
111              
112             # ── load($path) ───────────────────────────────────────────────────────────────
113             # Parse an existing manifest JSON file and return a blessed object.
114             sub load {
115 3     3 1 6749 my ($class, $path) = @_;
116 3 50       9 confess "ERROR NBI::Manifest::load: path required\n" unless defined $path;
117 3 50       54 confess "ERROR NBI::Manifest::load: '$path' not found\n" unless -f $path;
118              
119 3 50       84 open(my $fh, '<', $path)
120             or confess "ERROR NBI::Manifest::load: cannot open '$path': $!\n";
121 3         5 my $raw = do { local $/; <$fh> };
  3         13  
  3         84  
122 3         22 close $fh;
123              
124 3         20 my $data = JSON::PP->new->utf8->decode($raw);
125 3         14719 my $self = bless $data, $class;
126 3         6 $self->{_path} = $path;
127 3         15 return $self;
128             }
129              
130             # ── output($name) ─────────────────────────────────────────────────────────────
131             # Returns the absolute path of a named output file.
132             # Useful for chaining launchers: read a previous manifest to get output paths.
133             sub output {
134 2     2 1 4575 my ($self, $name) = @_;
135 2 50       5 confess "ERROR NBI::Manifest::output: name required\n" unless defined $name;
136 2 100       206 my $filename = $self->{outputs}{$name}
137             or confess "ERROR NBI::Manifest::output: no output named '$name'\n";
138 1         3 return "$self->{outdir}/$filename";
139             }
140              
141             # ── update(%changes) ──────────────────────────────────────────────────────────
142             # Merge %changes into the object and rewrite to the stored _path.
143             # Used by nbilaunch after submission to record the Slurm job ID.
144             sub update {
145 3     3 1 2108 my ($self, %changes) = @_;
146             confess "ERROR NBI::Manifest::update: no path known - call write() first\n"
147 3 100       140 unless defined $self->{_path};
148              
149 2         5 for my $k (keys %changes) {
150 5         8 $self->{$k} = $changes{$k};
151             }
152 2         5 $self->write($self->{_path});
153 2         6 return $self;
154             }
155              
156             1;
157              
158             __END__