sciolyff 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for one placing in the Placings section of a SciolyFF file
8
+ class Validator::Placings < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ event: String,
13
+ team: Integer
14
+ }.freeze
15
+
16
+ OPTIONAL = {
17
+ place: Integer,
18
+ participated: [true, false],
19
+ disqualified: [true, false],
20
+ exempt: [true, false],
21
+ tie: [true, false],
22
+ unknown: [true, false],
23
+ raw: Hash
24
+ }.freeze
25
+
26
+ def initialize(rep)
27
+ @events_by_name = rep[:Events].group_by { |e| e[:name] }
28
+ .transform_values(&:first)
29
+ @teams_by_number = rep[:Teams].group_by { |t| t[:number] }
30
+ .transform_values(&:first)
31
+ @event_names = @events_by_name.keys
32
+ @team_numbers = @teams_by_number.keys
33
+ @placings = rep[:Placings]
34
+ @maximum_place = rep[:Tournament][:'maximum place']
35
+ end
36
+
37
+ def matching_event?(placing, logger)
38
+ return true if @event_names.include? placing[:event]
39
+
40
+ logger.error "'event: #{placing[:event]}' in Placings "\
41
+ 'does not match any event name in Events'
42
+ end
43
+
44
+ def matching_team?(placing, logger)
45
+ return true if @team_numbers.include? placing[:team]
46
+
47
+ logger.error "'team: #{placing[:team]}' in Placings "\
48
+ 'does not match any team number in Teams'
49
+ end
50
+
51
+ def unique_event_and_team?(placing, logger)
52
+ return true if @placings.count do |other|
53
+ placing[:event] == other[:event] && placing[:team] == other[:team]
54
+ end == 1
55
+
56
+ logger.error "duplicate #{placing_log(placing)}"
57
+ end
58
+
59
+ def having_a_place_makes_sense?(placing, logger)
60
+ return true unless placing[:place] &&
61
+ (placing[:participated] == false ||
62
+ placing[:disqualified] ||
63
+ placing[:unknown] ||
64
+ placing[:raw])
65
+
66
+ logger.error 'having a place does not make sense for '\
67
+ "#{placing_log(placing)}"
68
+ end
69
+
70
+ def having_a_raw_makes_sense?(placing, logger)
71
+ return true unless placing[:raw] &&
72
+ (placing[:participated] == false ||
73
+ placing[:disqualified] ||
74
+ placing[:unknown] ||
75
+ placing[:place])
76
+
77
+ logger.error 'having raw section does not make sense for '\
78
+ "#{placing_log(placing)}"
79
+ end
80
+
81
+ def possible_participated_disqualified_combination?(placing, logger)
82
+ return true unless placing[:participated] == false &&
83
+ placing[:disqualified]
84
+
85
+ logger.error 'impossible participation-disqualified combination for '\
86
+ "#{placing_log(placing)}"
87
+ end
88
+
89
+ def possible_unknown_disqualified_combination?(placing, logger)
90
+ return true unless placing[:unknown] && placing[:disqualified]
91
+
92
+ logger.error 'impossible unknown-disqualified combination for '\
93
+ "#{placing_log(placing)}"
94
+ end
95
+
96
+ def unknown_allowed?(placing, logger)
97
+ event = @events_by_name[placing[:event]]
98
+ team = @teams_by_number[placing[:team]]
99
+ return true unless invalid_unknown?(placing, event, team)
100
+
101
+ logger.error "unknown place not allowed for #{placing_log(placing)} "\
102
+ '(either placing must be exempt or event must be trial/trialed)'
103
+ end
104
+
105
+ private
106
+
107
+ def placing_log(placing)
108
+ "placing with 'team: #{placing[:team]}' and 'event: #{placing[:event]}'"
109
+ end
110
+
111
+ def invalid_unknown?(placing, event, team)
112
+ placing[:unknown] &&
113
+ @maximum_place.nil? &&
114
+ !placing[:exempt] &&
115
+ !event[:trial] &&
116
+ !event[:trialed] &&
117
+ !team[:exhibition]
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for one raw of a placing in the Placings section of a SciolyFF file
8
+ class Validator::Raws < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ score: Float
13
+ }.freeze
14
+
15
+ OPTIONAL = {
16
+ 'tiebreaker rank': Integer
17
+ }.freeze
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Generic tests for (sub-)sections and types. Including classes must have two
5
+ # hashes REQUIRED and OPTIONAL (see other files in this dir for examples)
6
+ module Validator::Sections
7
+ def all_required_sections?(rep, logger)
8
+ missing = self.class::REQUIRED.keys - rep.keys
9
+ return true if missing.empty?
10
+
11
+ logger.error "missing required sections: #{missing.join ', '}"
12
+ end
13
+
14
+ def no_extra_sections?(rep, logger)
15
+ extra = rep.keys - (self.class::REQUIRED.keys + self.class::OPTIONAL.keys)
16
+ return true if extra.empty?
17
+
18
+ logger.error "extra section(s) found: #{extra.join ', '}"
19
+ end
20
+
21
+ def sections_are_correct_type?(rep, logger)
22
+ correct_types = self.class::REQUIRED.merge self.class::OPTIONAL
23
+ rep.all? do |key, value|
24
+ correct = correct_types[key]
25
+ next true if (correct.instance_of?(Array) && correct.include?(value)) ||
26
+ (value.instance_of? correct)
27
+
28
+ logger.error "#{key}: #{value} is not #{correct}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for one team in the Teams section of a SciolyFF file
8
+ class Validator::Teams < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ number: Integer,
13
+ school: String,
14
+ state: String
15
+ }.freeze
16
+
17
+ OPTIONAL = {
18
+ 'school abbreviation': String,
19
+ subdivision: String,
20
+ suffix: String,
21
+ city: String,
22
+ disqualified: [true, false],
23
+ exhibition: [true, false]
24
+ }.freeze
25
+
26
+ def initialize(rep)
27
+ initialize_teams_info(rep[:Teams])
28
+ @placings = rep[:Placings].group_by { |p| p[:team] }
29
+ @exempt = rep[:Tournament][:'exempt placings'] || 0
30
+ end
31
+
32
+ def unique_number?(team, logger)
33
+ return true if @numbers.count(team[:number]) == 1
34
+
35
+ logger.error "duplicate team number: #{team[:number]}"
36
+ end
37
+
38
+ def unique_suffix_per_school?(team, logger)
39
+ full_school = [team[:school], team[:city], team[:state]]
40
+ return true if @schools[full_school].count do |other|
41
+ !other[:suffix].nil? && other[:suffix] == team[:suffix]
42
+ end <= 1
43
+
44
+ logger.error "team number #{team[:number]} has the same suffix "\
45
+ 'as another team from the same school'
46
+ end
47
+
48
+ def unambiguous_cities_per_school?(team, logger)
49
+ return true unless @schools.keys.find do |other|
50
+ team[:city].nil? && !other[1].nil? &&
51
+ team[:school] == other[0] &&
52
+ team[:state] == other[2]
53
+ end
54
+
55
+ logger.error "city for team number #{team[:number]} is ambiguous, "\
56
+ 'value is required for unambiguity'
57
+ end
58
+
59
+ def correct_number_of_exempt_placings?(team, logger)
60
+ count = @placings[team[:number]].count { |p| p[:exempt] }
61
+ return true if count == @exempt || team[:exhibition]
62
+
63
+ logger.error "'team: #{team[:number]}' has incorrect number of "\
64
+ "exempt placings (#{count} insteand of #{@exempt})"
65
+ end
66
+
67
+ def in_a_subdivision_if_possible?(team, logger)
68
+ return true unless @subdivisions && !team[:subdivision]
69
+
70
+ logger.warn "missing subdivision for 'team: #{team[:number]}'"
71
+ end
72
+
73
+ private
74
+
75
+ def initialize_teams_info(teams)
76
+ @numbers = teams.map { |t| t[:number] }
77
+ @schools = teams.group_by { |t| [t[:school], t[:city], t[:state]] }
78
+ @subdivisions = teams.find { |t| t[:subdivision] }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Top-level sections of a SciolyFF file
8
+ class Validator::TopLevel < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ Tournament: Hash,
13
+ Events: Array,
14
+ Teams: Array,
15
+ Placings: Array
16
+ }.freeze
17
+
18
+ OPTIONAL = {
19
+ Penalties: Array
20
+ }.freeze
21
+ end
22
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for Tournament section of a SciolyFF file
8
+ class Validator::Tournament < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ location: String,
13
+ level: %w[Invitational Regionals States Nationals],
14
+ division: %w[A B C],
15
+ year: Integer,
16
+ date: Date
17
+ }.freeze
18
+
19
+ OPTIONAL = {
20
+ name: String,
21
+ state: String,
22
+ 'short name': String,
23
+ 'worst placings dropped': Integer,
24
+ 'exempt placings': Integer,
25
+ 'maximum place': Integer,
26
+ 'per-event n': [true, false],
27
+ 'n offset': Integer
28
+ }.freeze
29
+
30
+ def initialize(rep)
31
+ @maximum_place = rep[:Teams].count { |t| !t[:exhibition] }
32
+ end
33
+
34
+ def name_for_not_states_or_nationals?(tournament, logger)
35
+ level = tournament[:level]
36
+ return true if %w[States Nationals].include?(level) || tournament[:name]
37
+
38
+ logger.error 'name for Tournament required '\
39
+ "('level: #{level}' is not States or Nationals)"
40
+ end
41
+
42
+ def state_for_not_nationals?(tournament, logger)
43
+ return true if tournament[:level] == 'Nationals' || tournament[:state]
44
+
45
+ logger.error 'state for Tournament required '\
46
+ "('level: #{tournament[:level]}' is not Nationals)"
47
+ end
48
+
49
+ def short_name_is_relevant?(tournament, logger)
50
+ return true unless tournament[:'short name'] && !tournament[:name]
51
+
52
+ logger.error "'short name: #{tournament[:'short name']}' for Tournament "\
53
+ "requires a normal 'name:' as well"
54
+ end
55
+
56
+ def short_name_is_short?(tournament, logger)
57
+ return true if tournament[:'short name'].nil? ||
58
+ tournament[:'short name'].length < tournament[:name].length
59
+
60
+ logger.error "'short name: #{tournament[:'short name']}' for Tournament "\
61
+ "is longer than normal 'name: #{tournament[:name]}'"
62
+ end
63
+
64
+ def maximum_place_within_range?(tournament, logger)
65
+ return true if tournament[:'maximum place'].nil? ||
66
+ tournament[:'maximum place'].between?(1, @maximum_place)
67
+
68
+ logger.error "custom 'maximum place: #{tournament[:'maximum place']}' "\
69
+ "is not within range [1, #{@maximum_place}]"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Checks if SciolyFF YAML files and/or representations (i.e. hashes that can
5
+ # be directly converted to YAML) comply with spec (i.e. safe for interpreting)
6
+ class Validator
7
+ require 'sciolyff/validator/logger'
8
+ require 'sciolyff/validator/checker'
9
+ require 'sciolyff/validator/sections'
10
+
11
+ require 'sciolyff/validator/top_level'
12
+ require 'sciolyff/validator/tournament'
13
+ require 'sciolyff/validator/events'
14
+ require 'sciolyff/validator/teams'
15
+ require 'sciolyff/validator/placings'
16
+ require 'sciolyff/validator/penalties'
17
+ require 'sciolyff/validator/raws'
18
+
19
+ def initialize(loglevel = Logger::WARN)
20
+ @logger = Logger.new loglevel
21
+ @checkers = {}
22
+ end
23
+
24
+ def valid?(rep_or_file)
25
+ @logger.flush
26
+
27
+ if rep_or_file.instance_of? String
28
+ valid_file?(rep_or_file, @logger)
29
+ else
30
+ valid_rep?(rep_or_file, @logger)
31
+ end
32
+ end
33
+
34
+ def last_log
35
+ @logger.log
36
+ end
37
+
38
+ private
39
+
40
+ def valid_rep?(rep, logger)
41
+ unless rep.instance_of? Hash
42
+ logger.error 'improper file structure'
43
+ return false
44
+ end
45
+
46
+ result = check_all(rep, logger)
47
+
48
+ @checkers.clear # aka this method is not thread-safe
49
+ result
50
+ end
51
+
52
+ def valid_file?(path, logger)
53
+ rep = YAML.safe_load(
54
+ File.read(path),
55
+ permitted_classes: [Date],
56
+ symbolize_names: true
57
+ )
58
+ rescue StandardError => e
59
+ logger.error "could not read file as YAML:\n#{e.message}"
60
+ else
61
+ valid_rep?(rep, logger)
62
+ end
63
+
64
+ def check_all(rep, logger)
65
+ check(TopLevel, rep, rep, logger) &&
66
+ check(Tournament, rep, rep[:Tournament], logger) &&
67
+ [Events, Teams, Placings, Penalties].all? do |klass|
68
+ check_list(klass, rep, logger)
69
+ end &&
70
+ rep[:Placings].map { |p| p[:raw] }.compact.all? do |r|
71
+ check(Raws, rep, r, logger)
72
+ end
73
+ end
74
+
75
+ def check_list(klass, rep, logger)
76
+ key = klass.to_s.split('::').last.to_sym
77
+ return true unless rep.key? key # ignore optional sections like Penalties
78
+
79
+ rep[key].map { |e| check(klass, rep, e, logger) }.all?
80
+ end
81
+
82
+ def check(klass, top_level_rep, rep, logger)
83
+ @checkers[klass] ||= klass.new top_level_rep
84
+ checks = klass.instance_methods - Checker.instance_methods
85
+ checks.map { |im| @checkers[klass].send im, rep, logger }.all?
86
+ end
87
+ end
88
+ end
data/lib/sciolyff.rb CHANGED
@@ -2,49 +2,6 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'date'
5
- require 'sciolyff/top_level'
6
- require 'sciolyff/sections'
7
- require 'sciolyff/tournament'
8
- require 'sciolyff/events'
9
- require 'sciolyff/teams'
10
- require 'sciolyff/placings'
11
- require 'sciolyff/scores'
12
- require 'sciolyff/penalties'
13
- require 'sciolyff/interpreter'
14
-
15
- # API methods for the Scioly File Format
16
- #
17
- module SciolyFF
18
- class << self
19
- attr_accessor :rep
20
- end
21
-
22
- # Assumes rep is the output of YAML.load
23
- def self.validate(rep, opts: {})
24
- SciolyFF.rep = rep
25
-
26
- mt_args = []
27
- mt_args << '--verbose' if opts[:verbose]
28
-
29
- Minitest.run mt_args
30
- end
31
5
 
32
- def self.validate_file(path, opts: {})
33
- file = File.read(path)
34
- rep = YAML.safe_load(file, permitted_classes: [Date], symbolize_names: true)
35
- rescue StandardError => e
36
- puts 'Error: could not read file as YAML.'
37
- warn e.message
38
- else
39
- puts FILE_VALIDATION_MESSAGE
40
- validate(rep, opts: opts)
41
- end
42
-
43
- FILE_VALIDATION_MESSAGE = <<~STRING
44
- Validating file with Minitest...
45
-
46
- Overkill? Probably.
47
- Doesn't give line numbers from original file? Yeah.
48
-
49
- STRING
50
- end
6
+ require 'sciolyff/interpreter'
7
+ require 'sciolyff/validator'
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sciolyff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Em Zhan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-20 00:00:00.000000000 Z
11
+ date: 2020-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: minitest
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '5.11'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '5.11'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: optimist
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -61,22 +47,31 @@ extra_rdoc_files: []
61
47
  files:
62
48
  - bin/sciolyff
63
49
  - lib/sciolyff.rb
64
- - lib/sciolyff/events.rb
65
50
  - lib/sciolyff/interpreter.rb
66
51
  - lib/sciolyff/interpreter/event.rb
52
+ - lib/sciolyff/interpreter/html.rb
53
+ - lib/sciolyff/interpreter/html/helpers.rb
54
+ - lib/sciolyff/interpreter/html/template.html.erb
67
55
  - lib/sciolyff/interpreter/model.rb
68
56
  - lib/sciolyff/interpreter/penalty.rb
69
57
  - lib/sciolyff/interpreter/placing.rb
58
+ - lib/sciolyff/interpreter/raw.rb
59
+ - lib/sciolyff/interpreter/subdivisions.rb
70
60
  - lib/sciolyff/interpreter/team.rb
61
+ - lib/sciolyff/interpreter/tiebreaks.rb
71
62
  - lib/sciolyff/interpreter/tournament.rb
72
63
  - lib/sciolyff/interpreter/ztestscript.rb
73
- - lib/sciolyff/penalties.rb
74
- - lib/sciolyff/placings.rb
75
- - lib/sciolyff/scores.rb
76
- - lib/sciolyff/sections.rb
77
- - lib/sciolyff/teams.rb
78
- - lib/sciolyff/top_level.rb
79
- - lib/sciolyff/tournament.rb
64
+ - lib/sciolyff/validator.rb
65
+ - lib/sciolyff/validator/checker.rb
66
+ - lib/sciolyff/validator/events.rb
67
+ - lib/sciolyff/validator/logger.rb
68
+ - lib/sciolyff/validator/penalties.rb
69
+ - lib/sciolyff/validator/placings.rb
70
+ - lib/sciolyff/validator/raws.rb
71
+ - lib/sciolyff/validator/sections.rb
72
+ - lib/sciolyff/validator/teams.rb
73
+ - lib/sciolyff/validator/top_level.rb
74
+ - lib/sciolyff/validator/tournament.rb
80
75
  homepage: https://github.com/zqianem/sciolyff
81
76
  licenses:
82
77
  - MIT
@@ -96,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
91
  - !ruby/object:Gem::Version
97
92
  version: '0'
98
93
  requirements: []
99
- rubygems_version: 3.0.6
94
+ rubygems_version: 3.1.2
100
95
  signing_key:
101
96
  specification_version: 4
102
97
  summary: A file format for Science Olympiad tournament results.
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'minitest/test'
4
- require 'set'
5
-
6
- module SciolyFF
7
- # Tests that also serve as the specification for the sciolyff file format
8
- #
9
- class Events < Minitest::Test
10
- def setup
11
- skip unless SciolyFF.rep.instance_of? Hash
12
- @events = SciolyFF.rep[:Events]
13
- skip unless @events.instance_of? Array
14
- end
15
-
16
- def test_has_valid_events
17
- @events.each do |event|
18
- assert_instance_of Hash, event
19
- end
20
- end
21
-
22
- def test_each_event_does_not_have_extra_info
23
- @events.select { |e| e.instance_of? Hash }.each do |event|
24
- info = Set.new %i[name trial trialed scoring tiers]
25
- assert Set.new(event.keys).subset? info
26
- end
27
- end
28
-
29
- def test_each_event_has_valid_name
30
- @events.select { |e| e.instance_of? Hash }.each do |event|
31
- assert_instance_of String, event[:name]
32
- end
33
- end
34
-
35
- def test_each_event_has_valid_trial
36
- @events.select { |e| e.instance_of? Hash }.each do |event|
37
- assert_includes [true, false], event[:trial] if event.key? :trial
38
- end
39
- end
40
-
41
- def test_each_event_has_valid_trialed
42
- @events.select { |e| e.instance_of? Hash }.each do |event|
43
- assert_includes [true, false], event[:trialed] if event.key? :trialed
44
- end
45
- end
46
-
47
- def test_each_event_has_valid_scoring
48
- @events.select { |e| e.instance_of? Hash }.each do |event|
49
- assert_includes %w[high low], event[:scoring] if event.key? :scoring
50
- end
51
- end
52
-
53
- def test_each_event_has_valid_tiers
54
- @events.select { |e| e.instance_of? Hash }.each do |event|
55
- assert_instance_of Integer, event[:tiers] if event.key? :tiers
56
- assert_includes (1..), event[:tiers] if event.key? :tiers
57
- end
58
- end
59
-
60
- def test_each_event_has_unique_name
61
- names = @events.select { |e| e.instance_of? Hash }
62
- .map { |e| e[:name] }
63
- assert_nil names.uniq!
64
- end
65
- end
66
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'minitest/test'
4
- require 'set'
5
-
6
- module SciolyFF
7
- # Tests that also serve as the specification for the sciolyff file format
8
- #
9
- class Penalties < Minitest::Test
10
- def setup
11
- skip unless SciolyFF.rep.instance_of? Hash
12
- @penalties = SciolyFF.rep[:Penalties]
13
- skip unless @penalties.instance_of? Array
14
- end
15
-
16
- def test_has_valid_penalties
17
- @penalties.each do |penalty|
18
- assert_instance_of Hash, penalty
19
- end
20
- end
21
-
22
- def test_each_penalty_does_not_have_extra_info
23
- @penalties.select { |p| p.instance_of? Hash }.each do |penalty|
24
- info = Set.new %i[team points]
25
- assert Set.new(penalty.keys).subset? info
26
- end
27
- end
28
-
29
- def test_each_penalty_has_valid_team
30
- @penalties.select { |p| p.instance_of? Hash }.each do |penalty|
31
- assert_instance_of Integer, penalty[:team]
32
- skip unless SciolyFF.rep[:Teams].instance_of? Array
33
-
34
- team_numbers = SciolyFF.rep[:Teams].map { |t| t[:number] }
35
- assert_includes team_numbers, penalty[:team]
36
- end
37
- end
38
-
39
- def test_each_penalty_has_valid_points
40
- @penalties.select { |p| p.instance_of? Hash }.each do |penalty|
41
- assert_instance_of Integer, penalty[:points]
42
- assert penalty[:points] >= 0
43
- end
44
- end
45
-
46
- def test_penalties_are_unique_for_team
47
- teams = @penalties.select { |p| p.instance_of? Hash }
48
- .map { |p| p[:team] }
49
- assert_nil teams.uniq!
50
- end
51
- end
52
- end