sequenceserver 1.1.0.beta2 → 1.1.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +16 -5
  3. data/README.md +3 -0
  4. data/lib/sequenceserver/api_errors.rb +67 -0
  5. data/lib/sequenceserver/blast/constants.rb +1 -1
  6. data/lib/sequenceserver/blast/formatter.rb +4 -2
  7. data/lib/sequenceserver/blast/job.rb +17 -62
  8. data/lib/sequenceserver/blast/report.rb +15 -2
  9. data/lib/sequenceserver/blast.rb +1 -0
  10. data/lib/sequenceserver/exceptions.rb +2 -3
  11. data/lib/sequenceserver/job.rb +72 -44
  12. data/lib/sequenceserver/routes.rb +29 -25
  13. data/lib/sequenceserver/version.rb +1 -1
  14. data/lib/sequenceserver.rb +32 -20
  15. data/public/js/errormodal.js +55 -0
  16. data/public/js/report.js +6 -2
  17. data/public/js/sequenceserver.js +0 -12
  18. data/public/sequenceserver-report.min.js +15 -15
  19. data/public/sequenceserver-search.min.js +3 -3
  20. data/sequenceserver.gemspec +2 -1
  21. data/spec/capybara_spec.rb +120 -21
  22. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhd +8 -0
  23. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhi +0 -0
  24. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhr +0 -0
  25. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nin +0 -0
  26. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nog +0 -0
  27. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsd +16 -0
  28. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsi +0 -0
  29. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsq +0 -0
  30. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.txt +8 -0
  31. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phd +9140 -0
  32. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phi +0 -0
  33. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phr +0 -0
  34. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pin +0 -0
  35. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pog +0 -0
  36. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psd +18280 -0
  37. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psi +0 -0
  38. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psq +0 -0
  39. data/spec/database/sample/proteins/uniprot/URL +1 -0
  40. data/spec/nucleotide_query.fa +21 -0
  41. data/spec/protein_query.fa +21 -0
  42. data/spec/routes_spec.rb +2 -0
  43. data/spec/sample_reports/blastn_sample/job.yaml +1 -0
  44. data/spec/sample_reports/blastn_sample/{rfile → stdout} +0 -0
  45. data/spec/sample_reports/blastp_sample/job.yaml +1 -0
  46. data/spec/sample_reports/blastp_sample/{rfile → stdout} +0 -0
  47. data/spec/sample_reports/blastx_sample/job.yaml +1 -0
  48. data/spec/sample_reports/blastx_sample/{rfile → stdout} +0 -0
  49. data/spec/sample_reports/no_hits_sample/job.yaml +1 -0
  50. data/spec/sample_reports/no_hits_sample/{rfile → stdout} +0 -0
  51. data/spec/sample_reports/tblastn_sample/job.yaml +1 -0
  52. data/spec/sample_reports/tblastn_sample/{rfile → stdout} +0 -0
  53. data/spec/sample_reports/tblastx_sample/job.yaml +1 -0
  54. data/spec/sample_reports/tblastx_sample/{rfile → stdout} +0 -0
  55. data/spec/sample_reports/with_hits_sample/job.yaml +1 -0
  56. data/spec/sample_reports/with_hits_sample/{rfile → stdout} +0 -0
  57. data/spec/sequenceserver_spec.rb +1 -1
  58. data/spec/spec_helper.rb +2 -16
  59. data/views/layout.erb +0 -45
  60. metadata +55 -16
  61. data/lib/sequenceserver/blast/exceptions.rb +0 -27
  62. data/views/400.erb +0 -29
  63. data/views/500.erb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cccfc5a0df6982dde1187b7f12d043092d6b72c4
4
- data.tar.gz: ce2dbf97a61770f721fd3501e150817e4e33f095
3
+ metadata.gz: 2e60bdefc921484208426a507ff32229e5945b1b
4
+ data.tar.gz: 85a162161cae976cc790ae3fed7b06ab81f11786
5
5
  SHA512:
6
- metadata.gz: 7c75136b0c361e18f5d5d6dedfd4ad3aa8a730d3002f7be1823a47e7c6e6c0de1451116d6369af6c6cb5431e17d30cf64e2049960449cfab31d2566d1c0a7228
7
- data.tar.gz: 4871277a05d19b600fc2611958623aa38b6328a1e271917aeedf840a0901550f2f76f86e6f59ee31793e95a946ab2ba092308622c8e30290b7065f2913719ccf
6
+ metadata.gz: c4511c2651001127d25f0b319984ed4cbc5b4e4d6dccde42823b1c33fd14a99e86045d8416da1d8e514e6825df44933359d32eafe1167e0bdb30243d2017a295
7
+ data.tar.gz: 3917c29dfb8be319b92029295f3996338195aa3bab09a176c625b22e1f7b7fe56588aedf8d16b8724aa07acdfbe35c1c3982a3f2a3bbf47d165745d11cef4523
data/.travis.yml CHANGED
@@ -6,7 +6,7 @@ rvm:
6
6
  - 2.5
7
7
 
8
8
  addons:
9
- firefox: "58.0"
9
+ sauce_connect: true
10
10
 
11
11
  branches:
12
12
  only:
@@ -14,15 +14,26 @@ branches:
14
14
  - master
15
15
 
16
16
  install:
17
- - wget -c "https://github.com/mozilla/geckodriver/releases/download/v0.20.0/geckodriver-v0.20.0-linux64.tar.gz"
18
- - mkdir geckodriver && tar xvf geckodriver-*.tar.gz -C geckodriver
19
- - export PATH="$PWD/geckodriver:$PATH"
20
17
  - wget -c "ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/2.6.0/ncbi-blast-2.6.0+-x64-linux.tar.gz"
21
18
  - tar xvf ncbi-blast-*.tar.gz
22
19
  - gem install bundler && bundle
23
20
  - bundle exec bin/sequenceserver -s -b ncbi-blast-2.6.0+/bin -d spec/database/sample
24
21
 
25
- script: xvfb-run bundle exec rspec
22
+ before_script:
23
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter && chmod +x ./cc-test-reporter
24
+ - ./cc-test-reporter before-build
25
+ script:
26
+ - bundle exec rspec --exclude-pattern 'capybara*' # unit tests
27
+ - platform='macOS 10.13' browserName=safari browserVersion=11 bundle exec rspec spec/capybara_spec.rb
28
+ - platform='macOS 10.13' browserName=chrome browserVersion=65 bundle exec rspec spec/capybara_spec.rb
29
+ - platform='macOS 10.13' browserName=firefox browserVersion=59 bundle exec rspec spec/capybara_spec.rb
30
+ - platform='Windows 10' browserName=microsoftedge browserVersion='16.16299' bundle exec rspec spec/capybara_spec.rb
31
+ - platform='Windows 10' browserName=chrome browserVersion=65 bundle exec rspec spec/capybara_spec.rb
32
+ - platform='Windows 10' browserName=firefox browserVersion=59 bundle exec rspec spec/capybara_spec.rb
33
+ - platform='Linux' browserName=chrome browserVersion=48 bundle exec rspec spec/capybara_spec.rb
34
+ - platform='Linux' browserName=firefox browserVersion=45 bundle exec rspec spec/capybara_spec.rb
35
+ after_script:
36
+ - ./cc-test-reporter after-build # --exit-code $TRAVIS_TEST_RESULT
26
37
 
27
38
  cache:
28
39
  directories:
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![build status](https://secure.travis-ci.org/wurmlab/sequenceserver.png)](https://travis-ci.org/wurmlab/sequenceserver)
2
+ [![Sauce test status](https://saucelabs.com/buildstatus/yeban)](https://saucelabs.com/u/yeban)
2
3
  [![code climate](https://codeclimate.com/github/wurmlab/sequenceserver/badges/gpa.svg)](https://codeclimate.com/github/wurmlab/sequenceserver)
3
4
  [![coverage](https://codeclimate.com/github/wurmlab/sequenceserver/badges/coverage.svg)](https://codeclimate.com/github/wurmlab/sequenceserver)
4
5
  [![gem version](https://badge.fury.io/rb/sequenceserver.svg)](http://rubygems.org/gems/sequenceserver)
@@ -6,6 +7,8 @@
6
7
 
7
8
  [![gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/wurmlab/sequenceserver)
8
9
 
10
+ [![browser matrix](https://saucelabs.com/browser-matrix/yeban.svg)](https://saucelabs.com/u/yeban)
11
+
9
12
  # SequenceServer - BLAST searching made easy!
10
13
 
11
14
  SequenceServer lets you rapidly set up a BLAST+ server with an intuitive user
@@ -0,0 +1,67 @@
1
+ module SequenceServer
2
+
3
+ # API errors have an http status, title, message, and additional information
4
+ # like stacktrace or information from program output.
5
+ class APIError < StandardError
6
+ end
7
+
8
+ # Job not found (404).
9
+ class NotFound < APIError
10
+ def http_status
11
+ 404
12
+ end
13
+
14
+ def title
15
+ 'Job not found'
16
+ end
17
+
18
+ def message
19
+ 'The requested job could not be found'
20
+ end
21
+ end
22
+
23
+ # Errors caused due to incorrect user input.
24
+ class InputError < APIError
25
+ def initialize(more_info)
26
+ @more_info = more_info
27
+ super
28
+ end
29
+
30
+ def http_status
31
+ 400
32
+ end
33
+
34
+ def title
35
+ 'Input error'
36
+ end
37
+
38
+ def message
39
+ <<MSG
40
+ Looks like there's a problem with one of the query sequences, selected
41
+ databases, or advanced parameters.
42
+ MSG
43
+ end
44
+
45
+ attr_reader :more_info
46
+ end
47
+
48
+ # Errors caused by everything other than invalid user input.
49
+ class SystemError < APIError
50
+ def http_status
51
+ 500
52
+ end
53
+
54
+ def title
55
+ 'Job failed'
56
+ end
57
+
58
+ def message
59
+ <<MSG
60
+ Sorry BLAST failed - please try again. If this message persists, there is a
61
+ problem with the server. In this case, please report the bug on our
62
+ <a href="https://github.com/wurmlab/sequenceserver/issues" target="_blank">
63
+ issue tracker</a>.
64
+ MSG
65
+ end
66
+ end
67
+ end
@@ -2,7 +2,7 @@
2
2
  module SequenceServer
3
3
  # Define constanst used by BLAST module.
4
4
  module BLAST
5
- ERROR_LINE = /\(CArgException.*\)\s(.*)/
5
+ ERROR_LINE = /Error:\s(.*)/
6
6
 
7
7
  ALGORITHMS = %w(blastn blastp blastx tblastn tblastx)
8
8
 
@@ -24,7 +24,7 @@ module SequenceServer
24
24
  attr_reader :format, :mime, :specifiers
25
25
 
26
26
  def file
27
- @file = File.join(File.dirname(archive_file), filename)
27
+ @file ||= File.join(File.dirname(archive_file), filename)
28
28
  end
29
29
 
30
30
  def filename
@@ -41,12 +41,14 @@ module SequenceServer
41
41
  " -outfmt '#{format} #{specifiers}'" \
42
42
  " -out '#{file}'"
43
43
  sys(command, path: config[:bin], dir: DOTDIR)
44
+ rescue CommandFailed => e
45
+ fail SystemError, e.stderr
44
46
  end
45
47
 
46
48
  def validate
47
49
  return true if archive_file && format &&
48
50
  File.exist?(archive_file)
49
- fail ArgumentError, <<MSG
51
+ fail InputError, <<MSG
50
52
  Incorrect request parameters. Please ensure that requested file name is
51
53
  correct and the file type is either xml or tsv.
52
54
  MSG
@@ -1,17 +1,10 @@
1
- require 'sequenceserver/pool'
2
1
  require 'sequenceserver/job'
3
2
 
4
- require_relative 'exceptions'
5
- require_relative 'constants'
6
-
7
3
  module SequenceServer
8
4
 
9
5
  # BLAST module.
10
6
  module BLAST
11
7
  # Extends SequenceServer::Job to describe a BLAST job.
12
- #
13
- # `BLAST::ArgumentError` and `BLAST::RuntimeError` signal errors
14
- # encountered when attempting a BLAST search.
15
8
  class Job < Job
16
9
 
17
10
  def initialize(params)
@@ -25,60 +18,23 @@ module SequenceServer
25
18
  end
26
19
  end
27
20
 
28
-
29
21
  attr_reader :advanced_params
22
+
30
23
  # :nodoc:
31
- # Attributes used by us.
32
- #
33
- # Should be considered private.
24
+ # Attributes used by us - should be considered private.
34
25
  attr_reader :method, :qfile, :databases, :options
35
26
 
36
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
37
- # rubocop:disable Metrics/MethodLength
38
- def run
39
- command = "#{method} -db '#{databases.map(&:name).join(' ')}'" \
40
- " -query '#{qfile}' #{options}"
41
-
42
- sys(command, stdout: rfile, stderr: efile, path: config[:bin])
43
- done!
44
- rescue CommandFailed => e
45
- # Capture error.
46
- status = e.exitstatus
47
- case status
48
- when 1 # error in query sequence or options; see [1]
49
- efile.open
50
-
51
- # Most of the time BLAST+ generates a verbose error message with
52
- # details we don't require. So we parse out the relevant lines.
53
- error = efile.each_line do |l|
54
- break Regexp.last_match[1] if l.match(ERROR_LINE)
55
- end
56
-
57
- # But sometimes BLAST+ returns the exact/ relevant error message.
58
- # Trying to parse such messages returns nil, and we use the error
59
- # message from BLAST+ as it is.
60
- error = efile.rewind && efile.read unless error.is_a? String
61
-
62
- efile.close
63
- fail ArgumentError, error
64
- when 2, 3, 4, 255 # see [1]
65
- efile.open
66
- error = efile.read
67
- efile.close
68
- fail RuntimeError.new(status, error)
69
- end
70
-
71
- success!
72
- end
73
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
74
- # rubocop:enable Metrics/MethodLength
75
-
76
- def rfile
77
- File.join(dir, 'rfile')
27
+ # Returns the command that will be executed. Job super class takes care
28
+ # of actual execution.
29
+ def command
30
+ @command ||= "#{method} -db '#{databases.map(&:name).join(' ')}'" \
31
+ " -query '#{qfile}' #{options}"
78
32
  end
79
33
 
80
- def efile
81
- File.join(dir, 'efile')
34
+ # BLAST's exit status is not definitive of success or error, so we
35
+ # override success? to define custom criteria. :TODO:
36
+ def success?
37
+ super
82
38
  end
83
39
 
84
40
  private
@@ -114,38 +70,37 @@ module SequenceServer
114
70
 
115
71
  def validate_method(method)
116
72
  return true if ALGORITHMS.include? method
117
- fail ArgumentError, 'BLAST algorithm should be one of:' \
73
+ fail InputError, 'BLAST algorithm should be one of:' \
118
74
  " #{ALGORITHMS.join(', ')}."
119
75
  end
120
76
 
121
77
  def validate_sequences(sequences)
122
78
  return true if sequences.is_a?(String) && !sequences.empty?
123
- fail ArgumentError, 'Sequences should be a non-empty string.'
79
+ fail InputError, 'Sequences should be a non-empty string.'
124
80
  end
125
81
 
126
82
  def validate_databases(database_ids)
127
83
  ids = Database.ids
128
84
  return true if database_ids.is_a?(Array) && !database_ids.empty? &&
129
85
  (ids & database_ids).length == database_ids.length
130
- fail ArgumentError, 'Database id should be one of:' \
131
- " #{ids.join("\n")}."
86
+ fail InputError, "Database id should be one of: #{ids.join("\n")}."
132
87
  end
133
88
 
134
89
  # Advanced options are specified by the user. Here they are checked for
135
90
  # interference with SequenceServer operations.
136
91
  #
137
- # Raise ArgumentError if an error has occurred.
92
+ # Raise InputError if an error has occurred.
138
93
  def validate_options(options)
139
94
  return true if !options || (options.is_a?(String) &&
140
95
  options.strip.empty?)
141
96
 
142
97
  unless allowed_chars.match(options)
143
- fail ArgumentError, 'Invalid characters detected in options.'
98
+ fail InputError, 'Invalid characters detected in options.'
144
99
  end
145
100
 
146
101
  if disallowed_options.match(options)
147
102
  failedopt = Regexp.last_match[0]
148
- fail ArgumentError, "Option \"#{failedopt}\" is prohibited."
103
+ fail InputError, "Option \"#{failedopt}\" is prohibited."
149
104
  end
150
105
 
151
106
  true
@@ -43,14 +43,27 @@ module SequenceServer
43
43
 
44
44
  # Generate report.
45
45
  def generate
46
- xml_ir = parse_xml File.read(Formatter.run(job.rfile, 'xml').file)
47
- tsv_ir = parse_tsv File.read(Formatter.run(job.rfile, 'custom_tsv').file)
46
+ assert_job_completed_successfully
47
+ xml_ir = parse_xml File.read(Formatter.run(job.stdout, 'xml').file)
48
+ tsv_ir = parse_tsv File.read(Formatter.run(job.stdout, 'custom_tsv').file)
48
49
  extract_program_info xml_ir
49
50
  extract_params xml_ir
50
51
  extract_stats xml_ir
51
52
  extract_queries xml_ir, tsv_ir
52
53
  end
53
54
 
55
+ def assert_job_completed_successfully
56
+ return true if job.success?
57
+
58
+ stderr = File.readlines(job.stderr)
59
+ if job.exitstatus == 1 # error in query sequence or options; see [1]
60
+ error = stderr.grep(ERROR_LINE)
61
+ error = stderr if error.empty?
62
+ fail InputError, error.join
63
+ end
64
+ fail SystemError, stderr.join
65
+ end
66
+
54
67
  # Make program name and program name + version available via `program`
55
68
  # and `program_version` attributes.
56
69
  def extract_program_info(ir)
@@ -1,2 +1,3 @@
1
1
  require_relative 'blast/job'
2
2
  require_relative 'blast/report'
3
+ require_relative 'blast/constants'
@@ -148,13 +148,12 @@ MSG
148
148
  # Raised if the 'sys' method could not successfully execute a shell command.
149
149
  class CommandFailed < StandardError
150
150
 
151
- def initialize(stdout, stderr, exitstatus)
151
+ def initialize(exitstatus, stdout: nil, stderr: nil)
152
+ @exitstatus = exitstatus
152
153
  @stdout = stdout
153
154
  @stderr = stderr
154
- @exitstatus = exitstatus
155
155
  end
156
156
 
157
157
  attr_reader :stdout, :stderr, :exitstatus
158
-
159
158
  end
160
159
  end
@@ -5,44 +5,46 @@ require 'digest/md5'
5
5
  require 'forwardable'
6
6
  require 'securerandom'
7
7
 
8
+ require 'sequenceserver/pool'
9
+ require 'sequenceserver/api_errors'
10
+
8
11
  module SequenceServer
9
- # Job super class.
12
+ # Abstract job super class.
13
+ #
14
+ # Provides a simple framework to store job data, execute shell commands
15
+ # asynchronously and capture stdout, stderr and exit status. Subclasses
16
+ # must provide a concrete implementation for `command` and may override
17
+ # any other methods as required.
10
18
  #
11
- # Provides access to global `config` and `logger` object as instance methods.
12
- # Sub-classes must at least define `run` instance method.
19
+ # Global `config` and `logger` object are available as instance methods.
20
+ #
21
+ # Singleton methods provide the facility to create and queue a job,
22
+ # fetch a job or all jobs, and delete a job.
13
23
  class Job
14
- UUID_PATTERN = /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/
15
-
16
24
  class << self
17
25
  # Creates and queues a job. Returns created job object.
18
- #
19
- # TODO: Implement dynamic dispatch.
20
26
  def create(params)
21
- queue BLAST::Job.new(params)
27
+ job = BLAST::Job.new(params)# TODO: Dynamic dispatch.
28
+ SequenceServer.pool.queue { job.run }
29
+ job
22
30
  end
23
31
 
24
- # Fetches job object for the given id.
25
- #
26
- # TODO: What if the given job id does not exist?
32
+ # Fetches job with the given id.
27
33
  def fetch(id)
28
- YAML.load_file(File.join(DOTDIR, id, 'job.yaml'))
29
- end
30
-
31
- def all
32
- jobdirs = Dir["#{DOTDIR}/*"].select{ |f| f =~ UUID_PATTERN }
33
- jobdirs.map{ |fname| fetch File.basename(fname) }
34
+ job_file = File.join(DOTDIR, id, 'job.yaml')
35
+ fail NotFound unless File.exist?(job_file)
36
+ YAML.load_file(job_file)
34
37
  end
35
38
 
39
+ # Deletes job with the given id.
36
40
  def delete(id)
37
41
  FileUtils.rm_r File.join(DOTDIR, id)
38
42
  end
39
43
 
40
- private
41
-
42
- # Queues given job on the thread pool. Returns job.
43
- def queue(job)
44
- SequenceServer.pool.queue { job.run }
45
- job
44
+ # Returns an Array of all jobs.
45
+ def all
46
+ Dir["#{DOTDIR}/**/job.yaml"].
47
+ map { |f| fetch File.basename File.dirname f }
46
48
  end
47
49
  end
48
50
 
@@ -66,32 +68,49 @@ module SequenceServer
66
68
  raise e
67
69
  end
68
70
 
69
- attr_reader :id, :completed_at
71
+ attr_reader :id, :completed_at, :exitstatus
70
72
 
71
- # How to execute the job.
73
+ # Shell out and execute the job.
72
74
  #
73
- # Subclasses should provide their own implementation. The method should
74
- # call `done!` and `success!`, as appropriate, to indicate if the job is
75
- # done and whether it was successful.
75
+ # NOTE: This method is called asynchronously by thread pool.
76
76
  def run
77
- raise "To be implemented."
77
+ sys(command, path: config[:bin], stdout: stdout, stderr: stderr)
78
+ done!
79
+ rescue CommandFailed => e
80
+ done! e.exitstatus
78
81
  end
79
82
 
80
- # Is the job done?
81
- def done?
82
- !!@done
83
+ # Returns shell command that will be executed. Subclass needs to provide a
84
+ # concrete implementation.
85
+ def command
86
+ raise "Not implemented."
83
87
  end
84
88
 
85
- # Was the job success?
89
+ # Where will the stdout be written to during execution and read from later.
90
+ def stdout
91
+ File.join(dir, 'stdout')
92
+ end
93
+
94
+ # Where will the stderr be written to during execution and read from later.
95
+ def stderr
96
+ File.join(dir, 'stderr')
97
+ end
98
+
99
+ # Was the job successful?
86
100
  def success?
87
- !!@success
101
+ exitstatus == 0
102
+ end
103
+
104
+ # Is the job done? Yes if exitstatus of the job is available. No otherwise.
105
+ def done?
106
+ !!@exitstatus
88
107
  end
89
108
 
90
109
  private
91
110
 
92
- # Save job state.
111
+ # Saves job object to a YAML file in job directory.
93
112
  def save
94
- File.write(File.join(dir, 'job.yaml'), to_yaml)
113
+ File.write(yfile, to_yaml)
95
114
  end
96
115
 
97
116
  # Save arbitrary blob of data for this job to a file. Returns absolute path
@@ -108,16 +127,20 @@ module SequenceServer
108
127
  filename
109
128
  end
110
129
 
111
- # Marks the job as done.
112
- def done!
113
- @completed_at = Time.now
114
- @done = true
115
- save
130
+ # Retrieve file from job dir with the given name. Raises RuntimeError if
131
+ # the file can't be found.
132
+ #
133
+ # NOTE: Not used.
134
+ def fetch(key)
135
+ filename = File.join(dir, key)
136
+ raise if !File.exist? filename
137
+ filename
116
138
  end
117
139
 
118
- # Marks the job as success.
119
- def success!
120
- @success = true
140
+ # Marks the job as done and save its exitstatus.
141
+ def done!(status = 0)
142
+ @completed_at = Time.now
143
+ @exitstatus = status
121
144
  save
122
145
  end
123
146
 
@@ -125,5 +148,10 @@ module SequenceServer
125
148
  def dir
126
149
  File.join(DOTDIR, id)
127
150
  end
151
+
152
+ # Where to write serialised job object.
153
+ def yfile
154
+ File.join(dir, 'job.yaml')
155
+ end
128
156
  end
129
157
  end
@@ -12,16 +12,11 @@ module SequenceServer
12
12
  disable :method_override
13
13
 
14
14
  # Ensure exceptions never leak out of the app. Exceptions raised within
15
- # the app must be handled by the app. We do this by attaching error
16
- # blocks to exceptions we know how to handle and attaching to Exception
17
- # as fallback.
15
+ # the app must be handled by the app.
18
16
  disable :show_exceptions, :raise_errors
19
17
 
20
18
  # Make it a policy to dump to 'rack.errors' any exception raised by the
21
- # app so that error handlers don't have to do it themselves. But for it
22
- # to always work, Exceptions defined by us should not respond to `code`
23
- # or `http_status` methods. Error blocks must explicitly set http
24
- # status, if needed, by calling `status` method.
19
+ # app.
25
20
  enable :dump_errors
26
21
 
27
22
  # We don't want Sinatra do setup any loggers for us. We will use our own.
@@ -89,8 +84,7 @@ module SequenceServer
89
84
  get '/:jid.json' do |jid|
90
85
  job = Job.fetch(jid)
91
86
  halt 202 unless job.done?
92
- rep = Report.generate job
93
- rep.to_json
87
+ Report.generate(job).to_json
94
88
  end
95
89
 
96
90
  # Returns base HTML. Rest happens client-side: polling for and rendering
@@ -132,24 +126,34 @@ module SequenceServer
132
126
  send_file out.file, :filename => out.filename, :type => out.mime
133
127
  end
134
128
 
135
- # This error block will only ever be hit if the user gives us a funny
136
- # sequence or incorrect advanced parameter. Well, we could hit this block
137
- # if someone is playing around with our HTTP API too.
138
- error BLAST::ArgumentError do
139
- status 400
129
+ # Catches any exception raised within the app and returns error message and
130
+ # the backtrace as response body. If the error class defines `http_status`
131
+ # instance method, its return value will be used to set HTTP status. HTTP
132
+ # status is set to 500 otherwise.
133
+ error 400..500 do
140
134
  error = env['sinatra.error']
141
- erb :'400', :layout => nil, :locals => { :error => error }
142
- end
143
135
 
144
- # This will catch any unhandled error and some very special errors. Ideally
145
- # we will never hit this block. If we do, there's a bug in SequenceServer
146
- # or something really weird going on. If we hit this error block we show
147
- # the stacktrace to the user requesting them to post the same to our Google
148
- # Group.
149
- error Exception, BLAST::RuntimeError do
150
- status 500
151
- error = env['sinatra.error']
152
- erb :'500', :layout => nil, :locals => { :error => error }
136
+ # All errors will have a message.
137
+ error_data = {message: error.message}
138
+
139
+ # If error object has a title method, use that, or use error class for
140
+ # title.
141
+ if error.respond_to? :title
142
+ error_data[:title] = error.title
143
+ else
144
+ error_data[:title] = error.class.to_s
145
+ end
146
+
147
+ # If error object has a more_info method, use that. If the error does
148
+ # not have more_info and the error is not APIError, use backtrace as
149
+ # more_info.
150
+ if error.respond_to? :more_info
151
+ error_data[:more_info] = error.more_info
152
+ else
153
+ error_data[:more_info] = error.backtrace.join("\n")
154
+ end
155
+
156
+ error_data.to_json
153
157
  end
154
158
  end
155
159
  end
@@ -1,4 +1,4 @@
1
1
  # Define version number.
2
2
  module SequenceServer
3
- VERSION = '1.1.0.beta2'
3
+ VERSION = '1.1.0.beta3'
4
4
  end