@@ -18,3 +18,119 @@ require 'bundler/gem_tasks'
1818require 'rspec/core/rake_task'
1919
2020task :default => :spec
21+
22+
23+ # Pass a match expression as an optional argument to only run mutant
24+ # on classes that match. Example: `rake mutant TaxTribs::ZendeskSender`
25+ #
26+ task :mutant do
27+ current_ci_branch = ENV [ 'CIRCLE_BRANCH' ]
28+
29+ # Do not run mutant on CI master or develop branches.
30+ # Mutant will only run on pull requests against modified files.
31+ #
32+ if %w( master develop ) . include? ( current_ci_branch )
33+ puts "> CI #{ current_ci_branch } branch -- mutant will not run"
34+ exit
35+ end
36+
37+ mutation_type = ARGV [ 1 ]
38+
39+ vars = 'RAILS_ENV=test NOCOVERAGE=true' . freeze
40+ flags = [ '--use rspec' , '--fail-fast' ]
41+
42+ # When running on CI, a very high number of concurrent jobs seem to make some
43+ # mutations fail randomly. Reducing the number of jobs minimise this problem.
44+ flags . push ( '--jobs 24' ) if current_ci_branch
45+
46+ # This is to avoid running the mutant with flag `--since master` when
47+ # we are already on master, as otherwise it will not work as expected.
48+ current_branch = current_ci_branch || `git rev-parse --abbrev-ref HEAD`
49+
50+ if mutation_type == 'master'
51+ puts "> current branch: #{ current_branch } "
52+
53+ if current_branch != 'master'
54+ puts "> running complete mutant testing on all changes since 'origin/master'"
55+ flags . push ( '--since origin/master' )
56+ else
57+ # As we are already in master, let's not use the --since, and fallback to
58+ # running the quick randomised sample
59+ puts "> already on master, overriding --since flag"
60+ mutation_type = nil
61+ end
62+ end
63+
64+ unless system ( "#{ vars } mutant #{ flags . join ( ' ' ) } #{ classes_to_mutate ( mutation_type ) . join ( ' ' ) } " )
65+ raise 'Mutation testing failed'
66+ end
67+
68+ exit
69+ end
70+
71+ private
72+
73+ def classes_to_mutate ( option )
74+
75+ case option
76+ when nil
77+ # Quicker run, reduced testing scope (random sample), default option
78+ puts '> running quick sample mutant testing'
79+ form_objects . sample ( 5 ) + decision_trees_and_services . sample ( 3 ) + models
80+ when 'all' , 'master'
81+ # Complete coverage (very long run time) or only changed classes
82+ puts '> running complete mutant testing'
83+ form_objects + decision_trees_and_services + models + jobs
84+ else
85+ # Individual class testing, very quick
86+ # we'll take all arguments after the first (which is 'mutant')
87+ _foo , *classes = ARGV
88+ classes
89+ end
90+ end
91+
92+ # Classes that, for different reasons, mutant can't cope with or gives
93+ # false positives (for example, due to multi-threading tests, etc.)
94+ #
95+ def excluded_constants
96+ [
97+ C100App ::PaymentsFlowControl ,
98+ ] . freeze
99+ end
100+
101+ # As the current models are just empty shells for ActiveRecord relationships,
102+ # and we don't even have corresponding spec tests for those, there is no point
103+ # in including these in the mutation test, and thus we can save some time.
104+ # Only include in this collection the models that matter and have specs.
105+ #
106+ def models
107+ %w(
108+ C100Application
109+ Applicant
110+ Solicitor
111+ User
112+ PaymentIntent
113+ CompletedApplicationsAudit
114+ ) . freeze
115+ end
116+
117+ # Everything inheriting from `BaseForm` and inside namespace `Steps`
118+ # i.e. all classes in `/app/forms/steps/**/*`
119+ #
120+ def form_objects
121+ BaseForm . descendants . map ( &:name ) . grep ( /^Steps::/ )
122+ end
123+
124+ # Everything inside `C100App` namespace, unless included in `#excluded_constants`
125+ # i.e. all classes in `/app/services/c100_app/*`
126+ #
127+ def decision_trees_and_services
128+ C100App . constants . map { |symbol | C100App . const_get ( symbol ) } - excluded_constants
129+ end
130+
131+ # Everything inheriting from `ApplicationJob`
132+ # i.e. all jobs in `/app/jobs/*`
133+ #
134+ def jobs
135+ ApplicationJob . descendants
136+ end
0 commit comments