proformaxml 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="urn:proforma:tests:java-checkstyle:v1.1" targetNamespace="urn:proforma:tests:java-checkstyle:v1.1" elementFormDefault="qualified" attributeFormDefault="unqualified">
3
+ <xs:element name="java-checkstyle">
4
+ <xs:complexType>
5
+ <xs:sequence>
6
+ <xs:element name="max-checkstyle-warnings" type="xs:string"/>
7
+ </xs:sequence>
8
+ <xs:attribute name="version" type="xs:string" use="required"/>
9
+ </xs:complexType>
10
+ </xs:element>
11
+ </xs:schema>
@@ -0,0 +1,50 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!-- This is meant for blackbox tests of the output of a program. For regexp tests of
3
+ sourcecode something like checkstyle can be used.-->
4
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="urn:proforma:tests:regexptest:v0.9"
5
+ targetNamespace="urn:proforma:tests:regexptest:v0.9" elementFormDefault="qualified"
6
+ attributeFormDefault="unqualified">
7
+ <xs:element name="regexptest">
8
+ <xs:complexType>
9
+ <xs:sequence>
10
+ <!-- entry-point is a file or a main-class needed to start a program -->
11
+ <xs:element name="entry-point" type="xs:string"/>
12
+ <!-- parameter: optional list of command line arguments -->
13
+ <xs:element name="parameter" minOccurs="0">
14
+ <xs:simpleType>
15
+ <xs:list itemType="xs:string"/>
16
+ </xs:simpleType>
17
+ </xs:element>
18
+ <xs:element name="regular-expressions">
19
+ <xs:complexType>
20
+ <!-- choice means: regexp-allow OR regexp-disallow -->
21
+ <xs:choice>
22
+ <xs:sequence>
23
+ <!-- regular expression for allowed program outputs -->
24
+ <xs:element name="regexp-allow" type="ns1:regexpType" maxOccurs="unbounded"/>
25
+ <!-- regular expression for not allowed program outputs -->
26
+ <xs:element name="regexp-disallow" type="ns1:regexpType" minOccurs="0" maxOccurs="unbounded"/>
27
+ </xs:sequence>
28
+ <xs:element name="regexp-disallow" type="ns1:regexpType" maxOccurs="unbounded"/>
29
+ </xs:choice>
30
+ </xs:complexType>
31
+ </xs:element>
32
+ </xs:sequence>
33
+ </xs:complexType>
34
+ </xs:element>
35
+
36
+ <xs:complexType name="regexpType">
37
+ <xs:simpleContent>
38
+ <xs:extension base="xs:string">
39
+ <xs:attributeGroup ref="ns1:regexp-flags"/>
40
+ </xs:extension>
41
+ </xs:simpleContent>
42
+ </xs:complexType>
43
+ <!-- attributes to define, which regexp-flags are applied to a specified regular expression -->
44
+ <xs:attributeGroup name="regexp-flags">
45
+ <xs:attribute name="case-insensitive" type="xs:boolean" default="false"/>
46
+ <xs:attribute name="dotall" type="xs:boolean" default="false"/>
47
+ <xs:attribute name="multiline" type="xs:boolean" default="false"/>
48
+ <xs:attribute name="free-spacing" type="xs:boolean" default="false"/>
49
+ </xs:attributeGroup>
50
+ </xs:schema>
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="urn:proforma:tests:unittest:v1.1" targetNamespace="urn:proforma:tests:unittest:v1.1" elementFormDefault="qualified" attributeFormDefault="unqualified">
3
+ <xs:element name="unittest">
4
+ <xs:complexType>
5
+ <xs:sequence>
6
+ <xs:element name="entry-point" type="xs:string" maxOccurs="unbounded"/>
7
+ </xs:sequence>
8
+ <xs:attribute name="framework" type="xs:string" use="required"/>
9
+ <xs:attribute name="version" type="xs:string" use="required"/>
10
+ </xs:complexType>
11
+ </xs:element>
12
+ </xs:schema>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ class ProformaError < StandardError; end
5
+
6
+ class PostGenerateValidationError < ProformaError; end
7
+
8
+ class PreImportValidationError < ProformaError; end
9
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ module Helpers
5
+ module ExportHelpers
6
+ def attach_file(xml, file)
7
+ if file.embed?
8
+ embed_file(file, xml)
9
+ else
10
+ xml.send "attached-#{file.binary ? 'bin' : 'txt'}-file", file.filename
11
+ end
12
+ end
13
+
14
+ def embed_file(file, xml)
15
+ if file.binary
16
+ xml.send :'embedded-bin-file', {filename: file.filename}, Base64.encode64(file.content)
17
+ else
18
+ xml.send :'embedded-txt-file', {filename: file.filename}, file.content
19
+ end
20
+ end
21
+
22
+ def add_description_to_xml(xml, description)
23
+ xml.send(:description, description) if description.present?
24
+ end
25
+
26
+ def add_test_configuration(xml, test)
27
+ xml.send(:'test-configuration') do
28
+ add_filerefs(xml, test) if test.files
29
+ add_configuration(xml, test.configuration) unless test.configuration.nil?
30
+ if test.meta_data
31
+ xml.send(:'test-meta-data') do
32
+ meta_data(xml, test.meta_data)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def inner_meta_data(xml, namespace, data)
39
+ data.each do |key, value|
40
+ case value.class.name
41
+ when 'Hash'
42
+ # underscore is used to disambiguate tag names from ruby methods
43
+ xml[namespace].send("#{key}_") do |meta_data_xml|
44
+ inner_meta_data(meta_data_xml, namespace, value)
45
+ end
46
+ else
47
+ xml[namespace].send("#{key}_", value)
48
+ end
49
+ end
50
+ end
51
+
52
+ def meta_data(xml, meta_data)
53
+ meta_data.each do |namespace, data|
54
+ inner_meta_data(xml, namespace, data)
55
+ end
56
+ end
57
+
58
+ def add_configuration(xml, configuration)
59
+ xml_snippet = Dachsfisch::JSON2XMLConverter.perform(json: configuration.to_json)
60
+ configuration.flat_map {|_, val| val['@xmlns'].to_a }.uniq.each do |namespace|
61
+ xml.doc.root.add_namespace(namespace[0], namespace[1])
62
+ end
63
+
64
+ xml << xml_snippet
65
+ end
66
+
67
+ def add_namespaces_to_header(header, custom_namespaces)
68
+ custom_namespaces.each do |namespace|
69
+ header["xmlns:#{namespace[:prefix]}"] = namespace[:uri]
70
+ end
71
+ end
72
+
73
+ def add_parent_uuid_and_lang_to_header(header)
74
+ header['lang'] = @task.language if @task.language.present?
75
+ header['parent-uuid'] = @task.parent_uuid if @task.parent_uuid.present?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ module Helpers
5
+ module ImportHelpers
6
+ CONFIGURATION_NODES = %w[filerefs timeout externalresourcerefs test-meta-data].freeze
7
+
8
+ def set_hash_value_if_present(hash:, name:, attributes: nil, value_overwrite: nil)
9
+ raise unless attributes || value_overwrite
10
+
11
+ value = value_overwrite || attributes[name.to_s]&.value
12
+ hash[name.underscore.to_sym] = value if value.present?
13
+ end
14
+
15
+ def set_value_from_xml(object:, node:, name:, attribute: false, check_presence: true)
16
+ value = value_from_node(name, node, attribute)
17
+ return if check_presence && value.blank?
18
+
19
+ set_value(object:, name: (name.is_a?(Array) ? name[1] : name).underscore, value:)
20
+ end
21
+
22
+ def set_value(object:, name:, value:)
23
+ if object.is_a? Hash
24
+ object[name] = value
25
+ else
26
+ object.send("#{name}=", value)
27
+ end
28
+ end
29
+
30
+ def embedded_file_attributes(attributes, file_tag)
31
+ shared = shared_file_attributes(attributes, file_tag)
32
+ shared.merge(
33
+ content: content_from_file_tag(file_tag, shared[:binary])
34
+ ).tap do |hash|
35
+ hash[:filename] = file_tag.attributes['filename']&.value if file_tag.attributes['filename']&.value.present?
36
+ end
37
+ end
38
+
39
+ def attached_file_attributes(attributes, file_tag)
40
+ filename = file_tag.text
41
+ shared_file_attributes(attributes, file_tag).merge(filename:,
42
+ content: filestring_from_zip(filename))
43
+ end
44
+
45
+ def shared_file_attributes(attributes, file_tag)
46
+ {
47
+ id: attributes['id']&.value,
48
+ used_by_grader: attributes['used-by-grader']&.value == 'true',
49
+ visible: attributes['visible']&.value,
50
+ binary: file_tag.name.include?('-bin-file'),
51
+ }.tap do |hash|
52
+ set_hash_value_if_present(hash:, name: 'usage-by-lms', attributes:)
53
+ set_value_from_xml(object: hash, node: file_tag.parent, name: 'internal-description')
54
+ set_hash_value_if_present(hash:, name: 'mimetype', attributes:)
55
+ end
56
+ end
57
+
58
+ def add_test_configuration(test, test_node)
59
+ test_configuration_node = test_node.xpath('xmlns:test-configuration')
60
+ test.files = test_files_from_test_configuration(test_configuration_node)
61
+ test.configuration = extra_configuration_from_test_configuration(test_configuration_node)
62
+ meta_data_node = test_node.xpath('xmlns:test-configuration').xpath('xmlns:test-meta-data')
63
+ test.meta_data = meta_data(meta_data_node, use_namespace: true) if meta_data_node.present?
64
+ end
65
+
66
+ def extra_configuration_from_test_configuration(test_configuration_node)
67
+ configuration_any_nodes = test_configuration_node.children.reject {|c| CONFIGURATION_NODES.include? c.name }
68
+ return nil if configuration_any_nodes.empty?
69
+
70
+ {}.tap do |config_hash|
71
+ configuration_any_nodes.each do |config_node|
72
+ config_hash.merge! convert_xml_node_to_json(config_node)
73
+ end
74
+ end
75
+ end
76
+
77
+ def test_files_from_test_configuration(test_configuration_node)
78
+ files_from_filerefs(test_configuration_node.search('filerefs'))
79
+ end
80
+
81
+ def meta_data(any_data_node, use_namespace: false)
82
+ # use_namespace forces the use of the namespace as hash key - it should only be used at the entry of the recursion
83
+ {}.tap do |any_data|
84
+ any_data_node.children.each do |node|
85
+ key = (use_namespace ? node.namespace.prefix : any_data_node.name).to_sym
86
+ any_data[key] = if node.node_type == Nokogiri::XML::Node::TEXT_NODE
87
+ node.text
88
+ else
89
+ # preserve any existing data in the nested hash
90
+ (any_data[key] || {}).merge meta_data(node)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def convert_xml_node_to_json(any_node)
99
+ xml_snippet = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new, any_node)
100
+ xml_snippet.children.first.add_namespace_definition(any_node.namespace.prefix, any_node.namespace.href)
101
+ JSON.parse(Dachsfisch::XML2JSONConverter.perform(xml: xml_snippet.to_xml))
102
+ end
103
+
104
+ def value_from_node(name, node, attribute)
105
+ xml_name = name.is_a?(Array) ? name[0] : name
106
+ attribute ? node.attribute(xml_name)&.value : node.xpath("xmlns:#{xml_name}").text
107
+ end
108
+
109
+ def content_from_file_tag(file_tag, binary)
110
+ binary ? Base64.decode64(file_tag.text) : file_tag.text
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ class Base
5
+ include ActiveModel::AttributeAssignment
6
+
7
+ def initialize(attributes = {})
8
+ assign_attributes(attributes)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ class ModelSolution < Base
5
+ attr_accessor :id, :files, :description, :internal_description
6
+
7
+ def initialize(attributes = {})
8
+ super
9
+ self.files = [] if files.nil?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'proformaxml/models/base'
4
+ require 'proformaxml/models/task_file'
5
+ require 'proformaxml/models/test'
6
+ require 'proformaxml/models/model_solution'
7
+ require 'proformaxml/errors'
8
+
9
+ module ProformaXML
10
+ class Task < Base
11
+ attr_accessor :title, :description, :internal_description, :proglang, :uuid, :parent_uuid,
12
+ :language, :model_solutions, :files, :tests, :meta_data
13
+
14
+ def initialize(attributes = {})
15
+ super
16
+ self.files = [] if files.nil?
17
+ self.tests = [] if tests.nil?
18
+ self.model_solutions = [] if model_solutions.nil?
19
+ self.meta_data = {} if meta_data.nil?
20
+ end
21
+
22
+ def all_files
23
+ task_files = files
24
+ model_solution_files = model_solutions.map(&:files).filter(&:present?)
25
+ test_files = tests.map(&:files).filter(&:present?)
26
+ (task_files + model_solution_files + test_files).flatten.uniq
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ class TaskFile < Base
5
+ attr_accessor :id, :content, :filename, :used_by_grader, :visible, :usage_by_lms, :binary, :internal_description, :mimetype
6
+
7
+ def embed?
8
+ (content&.length || 0) < MAX_EMBEDDED_FILE_SIZE_KB * (2**10)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProformaXML
4
+ class Test < Base
5
+ attr_accessor :id, :title, :description, :internal_description, :test_type, :files, :configuration, :meta_data
6
+
7
+ def initialize(attributes = {})
8
+ super
9
+ self.files = [] if files.nil?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'proformaxml/helpers/export_helpers'
4
+
5
+ module ProformaXML
6
+ class Exporter
7
+ include ProformaXML::Helpers::ExportHelpers
8
+
9
+ def initialize(task:, custom_namespaces: [], version: nil)
10
+ @files = {}
11
+ @task = task
12
+ @custom_namespaces = custom_namespaces
13
+ @version = version || SCHEMA_VERSIONS.first
14
+ add_placeholders if @version == '2.0'
15
+ end
16
+
17
+ def perform
18
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
19
+ xml_task(xml)
20
+ end
21
+ xmldoc = builder.to_xml
22
+ doc = Nokogiri::XML(xmldoc)
23
+ errors = validate(doc)
24
+
25
+ raise PostGenerateValidationError.new(errors) if errors.any?
26
+
27
+ # File.open('../testfile.zip', 'wb') { |file| file.write(write_to_zip(xmldoc).string) }
28
+ write_to_zip(xmldoc)
29
+ end
30
+
31
+ private
32
+
33
+ def xml_task(xml)
34
+ xml.task(headers) do
35
+ xml.title @task.title
36
+ xml.description @task.description
37
+ add_internal_description_to_xml(xml, @task.internal_description)
38
+ xml.proglang({version: @task.proglang&.dig(:version)}, @task.proglang&.dig(:name))
39
+
40
+ add_objects_to_xml(xml)
41
+ add_meta_data(xml)
42
+ end
43
+ end
44
+
45
+ def add_internal_description_to_xml(xml, internal_description)
46
+ xml.send(:'internal-description', internal_description) if internal_description.present?
47
+ end
48
+
49
+ def add_meta_data(xml)
50
+ xml.send(:'meta-data') { meta_data(xml, @task.meta_data) }
51
+ end
52
+
53
+ def add_objects_to_xml(xml)
54
+ xml.files { files(xml) }
55
+ xml.send(:'model-solutions') { model_solutions(xml) } if @task.model_solutions.any? || @version == '2.0'
56
+ xml.tests { tests(xml) }
57
+ end
58
+
59
+ def files(xml)
60
+ @task.all_files.each do |file|
61
+ xml.file({
62
+ id: file.id, 'used-by-grader' => file.used_by_grader, visible: file.visible,
63
+ 'usage-by-lms' => file.usage_by_lms, mimetype: file.mimetype
64
+ }.compact) do
65
+ attach_file(xml, file)
66
+ add_internal_description_to_xml(xml, file.internal_description)
67
+ end
68
+ end
69
+ end
70
+
71
+ def model_solutions(xml)
72
+ @task.model_solutions&.each do |model_solution|
73
+ xml.send(:'model-solution', id: model_solution.id) do
74
+ add_filerefs(xml, model_solution)
75
+ add_description_to_xml(xml, model_solution.description)
76
+ add_internal_description_to_xml(xml, model_solution.internal_description)
77
+ end
78
+ end
79
+ end
80
+
81
+ def add_filerefs(xml, object)
82
+ return unless object.files.any?
83
+
84
+ xml.filerefs do
85
+ object.files.each do |file|
86
+ xml.fileref(refid: file.id) {}
87
+ end
88
+ end
89
+ end
90
+
91
+ def tests(xml)
92
+ @task.tests&.each do |test|
93
+ xml.test(id: test.id) do
94
+ xml.title test.title
95
+ add_description_to_xml(xml, test.description)
96
+ add_internal_description_to_xml(xml, test.internal_description)
97
+ xml.send(:'test-type', test.test_type)
98
+ add_test_configuration(xml, test)
99
+ end
100
+ end
101
+ end
102
+
103
+ # ms-placeholder only necessary for version 2.0 where model-solutions were mandatory
104
+ def add_placeholders
105
+ return if @task.model_solutions&.any?
106
+
107
+ file = TaskFile.new(content: '', id: 'ms-placeholder-file', used_by_grader: false, visible: 'no', binary: false)
108
+ model_solution = ModelSolution.new(id: 'ms-placeholder', files: [file])
109
+ @task.model_solutions = [model_solution]
110
+ end
111
+
112
+ def headers
113
+ {
114
+ 'xmlns' => "urn:proforma:v#{@version}",
115
+ 'uuid' => @task.uuid,
116
+ }.tap do |header|
117
+ add_namespaces_to_header(header, @custom_namespaces)
118
+ add_parent_uuid_and_lang_to_header(header)
119
+ end
120
+ end
121
+
122
+ def validate(doc)
123
+ validator = ProformaXML::Validator.new doc, @version
124
+ validator.perform
125
+ end
126
+
127
+ def write_to_zip(xmldoc)
128
+ Zip::OutputStream.write_buffer do |zio|
129
+ zio.put_next_entry('task.xml')
130
+ zio.write xmldoc
131
+ @task.all_files.each do |file|
132
+ next if file.embed?
133
+
134
+ zio.put_next_entry(file.filename)
135
+ zio.write file.content
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'proformaxml/helpers/import_helpers'
5
+
6
+ module ProformaXML
7
+ class Importer
8
+ include ProformaXML::Helpers::ImportHelpers
9
+
10
+ def initialize(zip:, expected_version: nil)
11
+ @zip = zip
12
+ @expected_version = expected_version
13
+
14
+ xml = filestring_from_zip('task.xml')
15
+ raise PreImportValidationError if xml.nil?
16
+
17
+ @doc = Nokogiri::XML(xml, &:noblanks)
18
+ @task = Task.new
19
+ end
20
+
21
+ def perform
22
+ errors = validate
23
+
24
+ raise PreImportValidationError.new(errors) if errors.any?
25
+
26
+ @task_node = @doc.xpath('/xmlns:task')
27
+
28
+ set_data
29
+ {task: @task, custom_namespaces: @custom_namespaces}
30
+ end
31
+
32
+ private
33
+
34
+ def filestring_from_zip(filename)
35
+ Zip::File.open(@zip.path) do |zip_file|
36
+ return zip_file.glob(filename).first&.get_input_stream&.read
37
+ end
38
+ end
39
+
40
+ def set_data
41
+ set_namespaces
42
+ set_base_data
43
+ set_files
44
+ set_model_solutions
45
+ set_tests
46
+ set_meta_data
47
+ end
48
+
49
+ def set_namespaces
50
+ @custom_namespaces = @doc.namespaces.except('xmlns').map {|k, v| {prefix: k[6..], uri: v} }
51
+ end
52
+
53
+ def set_base_data
54
+ set_value_from_xml(object: @task, node: @task_node, name: 'title')
55
+ set_value_from_xml(object: @task, node: @task_node, name: 'description')
56
+ set_value_from_xml(object: @task, node: @task_node, name: 'internal-description')
57
+ set_proglang
58
+ set_value_from_xml(object: @task, node: @task_node, name: %w[lang language], attribute: true)
59
+ set_value_from_xml(object: @task, node: @task_node, name: 'parent-uuid', attribute: true)
60
+ set_value_from_xml(object: @task, node: @task_node, name: 'uuid', attribute: true)
61
+ end
62
+
63
+ def set_proglang
64
+ return if @task_node.xpath('xmlns:proglang').text.blank?
65
+
66
+ @task.proglang = {name: @task_node.xpath('xmlns:proglang').text,
67
+ version: @task_node.xpath('xmlns:proglang').attribute('version').value.presence}.compact
68
+ end
69
+
70
+ def set_files
71
+ @task_node.search('files//file').each {|file_node| add_file file_node }
72
+ end
73
+
74
+ def set_tests
75
+ @task_node.search('tests//test').each {|test_node| add_test test_node }
76
+ end
77
+
78
+ def set_model_solutions
79
+ @task_node.search('model-solutions//model-solution').each do |model_solution_node|
80
+ add_model_solution model_solution_node
81
+ end
82
+ end
83
+
84
+ def set_meta_data
85
+ meta_data_node = @task_node.xpath('xmlns:meta-data')
86
+ @task.meta_data = meta_data(meta_data_node, use_namespace: true) if meta_data_node.text.present?
87
+ end
88
+
89
+ def add_model_solution(model_solution_node)
90
+ model_solution = ModelSolution.new
91
+ model_solution.id = model_solution_node.attributes['id'].value
92
+ model_solution.files = files_from_filerefs(model_solution_node.search('filerefs'))
93
+ set_value_from_xml(object: model_solution, node: model_solution_node, name: 'description')
94
+ set_value_from_xml(object: model_solution, node: model_solution_node, name: 'internal-description')
95
+ @task.model_solutions << model_solution unless model_solution.files.first&.id == 'ms-placeholder-file'
96
+ end
97
+
98
+ def add_file(file_node)
99
+ file_tag = file_node.children.first
100
+ file = nil
101
+ case file_tag.name
102
+ when /embedded-(bin|txt)-file/
103
+ file = TaskFile.new(embedded_file_attributes(file_node.attributes, file_tag))
104
+ when /attached-(bin|txt)-file/
105
+ file = TaskFile.new(attached_file_attributes(file_node.attributes, file_tag))
106
+ end
107
+ @task.files << file
108
+ end
109
+
110
+ def add_test(test_node)
111
+ test = Test.new
112
+ set_value_from_xml(object: test, node: test_node, name: 'id', attribute: true, check_presence: false)
113
+ set_value_from_xml(object: test, node: test_node, name: 'title', check_presence: false)
114
+ set_value_from_xml(object: test, node: test_node, name: 'description')
115
+ set_value_from_xml(object: test, node: test_node, name: 'internal-description')
116
+ set_value_from_xml(object: test, node: test_node, name: 'test-type', check_presence: false)
117
+ add_test_configuration(test, test_node)
118
+ @task.tests << test
119
+ end
120
+
121
+ def files_from_filerefs(filerefs_node)
122
+ [].tap do |files|
123
+ filerefs_node.search('fileref').each do |fileref_node|
124
+ fileref = fileref_node.attributes['refid'].value
125
+ files << @task.files.delete(@task.files.detect {|file| file.id == fileref })
126
+ end
127
+ end
128
+ end
129
+
130
+ def validate
131
+ validator = ProformaXML::Validator.new @doc, @expected_version
132
+ validator.perform
133
+ end
134
+ end
135
+ end