File Coverage

blib/lib/NBI/Job.pm
Criterion Covered Total %
statement 113 207 54.5
branch 35 84 41.6
condition 0 3 0.0
subroutine 15 24 62.5
pod 16 16 100.0
total 179 334 53.5


line stmt bran cond sub pod time code
1             package NBI::Job;
2             #ABSTRACT: A class for representing a job for NBI::Slurm
3              
4 16     16   310716 use 5.012;
  16         59  
5 16     16   97 use warnings;
  16         27  
  16         812  
6 16     16   104 use Carp qw(confess);
  16         23  
  16         1035  
7 16     16   5580 use Data::Dumper;
  16         76132  
  16         1067  
8 16     16   7390 use File::Spec::Functions;
  16         12916  
  16         1587  
9             $Data::Dumper::Sortkeys = 1;
10 16     16   111 use File::Basename;
  16         42  
  16         42725  
11              
12             $NBI::Job::VERSION = $NBI::Slurm::VERSION;
13             my $DEFAULT_QUEUE = "nbi-short";
14             require Exporter;
15             our @ISA = qw(Exporter);
16              
17              
18             sub new {
19 105     105 1 2202 my $class = shift @_;
20 105         218 my ($job_name, $commands_array, $command, $opts);
21              
22             # Descriptive instantiation with parameters -param => value
23 105 50       325 if (substr($_[0], 0, 1) eq '-') {
24 105         366 my %data = @_;
25             # Try parsing
26 105         317 for my $i (keys %data) {
27 311 100       1009 if ($i =~ /^-name/) {
    100          
    50          
    0          
28 105         238 $job_name = $data{$i};
29             } elsif ($i =~ /^-command$/) {
30 104         219 $command = $data{$i};
31             } elsif ($i =~ /^-opts$/) {
32             # Check that $data{$i} is an instance of NBI::Opts
33 102 50       408 if ($data{$i}->isa('NBI::Opts')) {
34             # $data{$i} is an instance of NBI::Opts
35 102         233 $opts = $data{$i};
36             } else {
37             # $data{$i} is not an instance of NBI::Opts
38 0         0 confess "ERROR NBI::Job: -opts must be an instance of NBI::Opts\n";
39             }
40            
41             } elsif ($i =~ /^-commands$/) {
42             # Check that $data{$i} is an array
43 0 0       0 if (ref($data{$i}) eq 'ARRAY') {
44 0         0 $commands_array = $data{$i};
45             } else {
46 0         0 confess "ERROR NBI::Job: -commands must be an array\n";
47             }
48             } else {
49 0         0 confess "ERROR NBI::Seq: Unknown parameter $i\n";
50             }
51             }
52             }
53            
54 105         255 my $self = bless {}, $class;
55            
56              
57 105 50       356 $self->{name} = defined $job_name ? $job_name : 'job-' . int(rand(1000000));
58 105         215 $self->{jobid} = 0;
59            
60             # Commands: if both commands_array and command are defined, append command to commands_array
61 105 50       256 if (defined $commands_array) {
    100          
62 0         0 $self->{commands} = $commands_array;
63 0 0       0 if (defined $command) {
64 0         0 push @{$self->{commands}}, $command;
  0         0  
65             }
66             } elsif (defined $command) {
67 104         292 $self->{commands} = [$command];
68             }
69              
70             # Opts must be an instance of NBI::Opts, check first
71 105 100       193 if (defined $opts) {
72             # check that $opts is an instance of NBI::Opts
73 102 50       268 if ($opts->isa('NBI::Opts')) {
74             # $opts is an instance of NBI::Opts
75 102         265 $self->{opts} = $opts;
76             } else {
77             # $opts is not an instance of NBI::Opts
78 0         0 confess "ERROR NBI::Job: -opts must be an instance of NBI::Opts\n";
79             }
80            
81             } else {
82 3         16 $self->{opts} = NBI::Opts->new($DEFAULT_QUEUE);
83             }
84              
85 105         211 $self->{script_path} = undef;
86              
87             # Check here if there is opts->placeholder in the commands.
88             # If there is then replace /placeholder/ with ${selected_file}
89              
90 105 100       231 if ($self->opts->is_array()) {
91 1         2 my $placeholder = $self->opts->placeholder;
92             # Double escape the backslash for regex
93 1         2 my $regex_placeholder = $placeholder;
94 1         16 $regex_placeholder =~ s/\\/\\\\/g;
95 1         2 my $count = 0;
96 1         1 for my $cmd (@{$self->{commands}}) {
  1         3  
97 1 50       2 if ($cmd =~ $self->opts->placeholder) {
98 1         1 $count++;
99             }
100 1         9 $cmd =~ s/\Q$regex_placeholder\E/\${selected_file}/g;
101             }
102             }
103 105         381 return $self;
104            
105             }
106              
107              
108             sub script_path : lvalue {
109             # Update script_path
110 0     0 1 0 my ($self, $new_val) = @_;
111 0 0       0 $self->{script_path} = $new_val if (defined $new_val);
112 0         0 return $self->{script_path};
113             }
114              
115             sub name : lvalue {
116             # Update name
117 409     409 1 1019 my ($self, $new_val) = @_;
118 409 50       883 $self->{name} = $new_val if (defined $new_val);
119 409         1546 return $self->{name};
120             }
121              
122             sub jobid : lvalue {
123             # Update jobid
124 0     0 1 0 my ($self, $new_val) = @_;
125 0 0 0     0 if (defined $new_val and $new_val !~ /^-?(\d+)$/) {
126 0         0 confess "ERROR NBI::Job: jobid must be an integer ". $new_val ."\n";
127             }
128 0 0       0 $self->{jobid} = $new_val if (defined $new_val);
129 0         0 return $self->{jobid};
130             }
131              
132             sub outputfile : lvalue {
133             # Update name
134 203     203 1 416 my ($self, $parameter) = @_;
135              
136 203         313 my $interpolate = 0;
137 203 50       470 if (defined $parameter) {
138 0 0       0 if ($parameter eq '-interpolate') {
139 0         0 $interpolate = 1;
140             } else {
141 0         0 $self->{output_file} = $parameter;
142             }
143             }
144              
145             # Create a default output_file if not defined
146 203 100       467 if (not defined $self->{output_file}) {
147 102         222 $self->{output_file} = catfile( $self->opts->tmpdir , $self->name . ".%j.out");
148             }
149            
150 203 50       479 if ($interpolate) {
151 0         0 my $jobid = $self->jobid;
152 0         0 my $output_file = $self->{output_file};
153 0         0 $output_file =~ s/%j/$jobid/g;
154 0         0 return $output_file;
155             } else {
156 203         492 return $self->{output_file};
157             }
158            
159             }
160              
161             sub errorfile : lvalue {
162             # Update name
163 203     203 1 380 my ($self, $parameter) = @_;
164              
165 203         311 my $interpolate = 0;
166 203 50       412 if (defined $parameter) {
167 0 0       0 if ($parameter eq '-interpolate') {
168 0         0 $interpolate = 1;
169             } else {
170 0         0 $self->{error_file} = $parameter;
171             }
172             }
173              
174             # Create a default error_file if not defined
175 203 100       446 if (not defined $self->{error_file}) {
176 102         197 $self->{error_file} = catfile( $self->opts->tmpdir , $self->name . ".%j.err");
177             }
178            
179 203 50       487 if ($interpolate) {
180 0         0 my $jobid = $self->jobid;
181 0         0 my $error_file = $self->{error_file};
182 0         0 $error_file =~ s/%j/$jobid/g;
183 0         0 return $error_file;
184             } else {
185 203         405 return $self->{error_file};
186             }
187            
188             }
189             sub append_command {
190 1     1 1 3 my ($self, $new_command) = @_;
191 1 50       2 if ($self->opts->is_array()) {
192 0         0 my $placeholder = $self->opts->placeholder;
193 0         0 $new_command =~ s/\Q$placeholder\E/\${selected_file}/g;
194             }
195 1         3 push @{$self->{commands}}, $new_command;
  1         2  
196             }
197              
198             sub prepend_command {
199 0     0 1 0 my ($self, $new_command) = @_;
200 0 0       0 if ($self->opts->is_array()) {
201 0         0 my $placeholder = $self->opts->placeholder;
202 0         0 $new_command =~ s/\Q$placeholder\E/\${selected_file}/g;
203             }
204 0         0 unshift @{$self->{commands}}, $new_command;
  0         0  
205             }
206              
207             sub commands {
208 0     0 1 0 my ($self) = @_;
209 0         0 return $self->{commands};
210             }
211              
212             sub commands_count {
213 4     4 1 15 my ($self) = @_;
214 4         6 return 0 + scalar @{$self->{commands}};
  4         18  
215             }
216              
217             sub set_opts {
218 1     1 1 5 my ($self, $opts) = @_;
219             # Check that $opts is an instance of NBI::Opts
220 1 50       7 if ($opts->isa('NBI::Opts')) {
221             # $opts is an instance of NBI::Opts
222 1         3 $self->{opts} = $opts;
223             } else {
224             # $opts is not an instance of NBI::Opts
225 0         0 confess "ERROR NBI::Job: -opts must be an instance of NBI::Opts\n";
226             }
227             }
228              
229             sub get_opts {
230 0     0 1 0 my ($self) = @_;
231 0         0 return $self->{opts};
232             }
233              
234             sub opts {
235 1131     1131 1 1870 my ($self) = @_;
236 1131         3307 return $self->{opts};
237             }
238              
239             ## Run job
240             sub script {
241 203     203 1 2456 my ($self) = @_;
242 203         554 my $template = [
243             '#SBATCH -J NBI_SLURM_JOBNAME',
244             '#SBATCH -o NBI_SLURM_OUT',
245             '#SBATCH -e NBI_SLURM_ERR',
246             ''
247             ];
248 203         403 my $header = $self->opts->header();
249            
250             # Replace the template
251 203         407 my $script = join("\n", @{$template});
  203         500  
252            
253             # Replace the values
254 203         557 my $name = $self->name;
255 203         549 my $file_out = $self->outputfile;
256 203         438 my $file_err = $self->errorfile;
257 203         956 $script =~ s/NBI_SLURM_JOBNAME/$name/g;
258 203         634 $script =~ s/NBI_SLURM_OUT/$file_out/g;
259 203         536 $script =~ s/NBI_SLURM_ERR/$file_err/g;
260            
261 203         343 my $replacements = 0;
262 203         380 my $placeholder = $self->opts->placeholder;
263            
264 203 100       401 if ($self->opts->is_array()) {
265            
266             # Prepend strings to array $self->{commands}
267             # Escape spaces in each file
268 1         2 my @prepend = ();
269 1         2 my $self_files = $self->opts->files;
270 1         1 for my $file (@{$self_files}) {
  1         2  
271 19         18 $file =~ s/ /\\ /g;
272             }
273 1         2 my $files_list = join(" ", @{$self_files});
  1         8  
274 1         1 my $list = "self_files=($files_list)";
275 1         3 push(@prepend, "# Job array list", "$list", "selected_file=\${self_files[\$SLURM_ARRAY_TASK_ID]}");
276            
277             # Prepend the array to the commands
278 1         1 unshift @{$self->{commands}}, @prepend;
  1         5  
279              
280            
281            
282             }
283 203 100       447 if ($self->opts->is_array()) {
284             # check if at least one command containts ${selected_file}
285 1         2 my $selected_file = 0;
286 1         1 for my $cmd (@{$self->{commands}}) {
  1         6  
287 4 100       7 if ($cmd =~ /\$\{selected_file\}/) {
288 1         1 $selected_file = 1;
289 1         15 last;
290             }
291             }
292 1 50       3 if ($selected_file == 0) {
293 0         0 confess "ERROR NBI::Job: No command contains the placeholder:" . $self->opts->placeholder . "\n";
294             }
295             }
296              
297 203         336 $script .= join("\n", @{$self->{commands}});
  203         498  
298 203         913 return $header . $script . "\n";
299             }
300              
301             sub run {
302 0     0 1   my $self = shift @_;
303             # Check it has some commands
304            
305            
306             # Check it has a queue
307 0 0         if (not defined $self->opts->queue) {
308 0           confess "ERROR NBI::Job: No queue defined for job " . $self->name . "\n";
309             }
310             # Check it has some opts
311 0 0         if (not defined $self->opts) {
312 0           confess "ERROR NBI::Job: No opts defined for job " . $self->name . "\n";
313             }
314             # Check it has some commands
315 0 0         if ($self->commands_count == 0) {
316 0           confess "ERROR NBI::Job: No commands defined for job " . $self->name . "\n";
317             }
318              
319             # Create the script
320 0           my $script = $self->script();
321              
322             # Create the script file
323 0           my $script_file = catfile($self->opts->tmpdir, $self->name . ".sh");
324              
325             # change suffix from .sh to .INT.sh if the file exists already
326 0 0         if (-e $script_file) {
327 0           my $i = 1;
328 0           while (-e $script_file) {
329 0           my $string_int = sprintf("%05d", $i);
330 0           $script_file = catfile($self->opts->tmpdir, $self->name . "." . $string_int . ".sh");
331 0           $i++;
332             }
333             }
334              
335 0           $self->{"script_path"} = $script_file;
336 0 0         open(my $fh, ">", $script_file) or confess "ERROR NBI::Job: Cannot open file $script_file for writing\n";
337 0           print $fh $script;
338 0           close($fh);
339              
340             # Run the script
341              
342 0 0         if (_has_command('sbatch') == 0) {
343 0           $self->jobid = -1;
344 0           return 0;
345             }
346 0           my $job_output = `sbatch "$script_file"`;
347              
348             # Check the output
349 0 0         if ($job_output =~ /Submitted batch job (\d+)/) {
350             # Job submitted
351 0           my $job_id = $1;
352             # Update the job id
353 0           $self->jobid = $job_id;
354 0           return $job_id;
355             } else {
356             # Job not submitted
357 0           confess "ERROR NBI::Job: Job " . $self->name . " not submitted\n";
358             }
359 0           return $self->jobid;
360             }
361              
362              
363             sub view {
364             # Return a string representation of the object
365 0     0 1   my $self = shift @_;
366 0           my $str = " --- NBI::Job object ---\n";
367 0           $str .= " name: " . $self->name . "\n";
368 0           $str .= " commands: \n\t" . join("\n\t", @{$self->commands}) . "\n";
  0            
369 0           $str .= " jobid: " . $self->jobid . "\n";
370 0           $str .= " script: " . $self->script_path . "\n";
371 0           $str .= " output file:" . $self->outputfile('-interpolate') . "\n";
372 0           $str .= " error file: " . $self->errorfile('-interpolate') . "\n";
373 0           $str .= " ---------------------------\n";
374            
375 0           return $str;
376             }
377              
378             sub _has_command {
379 0     0     my $command = shift;
380 0           my $is_available = 0;
381            
382 0 0         if ($^O eq 'MSWin32') {
383             # Windows system
384 0           $is_available = system("where $command >nul 2>nul") == 0;
385             } else {
386             # Unix-like system
387 0           $is_available = system("command -v $command >/dev/null 2>&1") == 0;
388             }
389            
390 0           return $is_available;
391             }
392              
393             sub _to_string {
394             # Convert string to a sanitized string with alphanumeric chars and dashes
395 0     0     my ($self, $string) = @_;
396 0           return $string =~ s/[^a-zA-Z0-9\-]//gr;
397             }
398             1;
399              
400             __END__