ceedling 1.1.9 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,6 +61,21 @@ describe GeneratorPartials do
61
61
  CExtractorTypes::CModule.new()
62
62
  end
63
63
 
64
+ # A StringIO standing in for the generated types header, with the filename lookup stubbed.
65
+ # Several examples differ only in what they hand generate_types, not in this plumbing.
66
+ def types_buffer(name, header_filename)
67
+ allow(@file_path_utils).to receive(:form_partial_types_header_filename)
68
+ .with(name)
69
+ .and_return(header_filename)
70
+
71
+ buf = StringIO.new()
72
+ allow(@file_wrapper).to receive(:open)
73
+ .with(File.join('/path/to/output', header_filename), 'wb')
74
+ .and_yield(buf)
75
+
76
+ buf
77
+ end
78
+
64
79
  context "#generate_implementation" do
65
80
  it "should call generate_header() and generate_source() with correct parameters" do
66
81
  # Setup
@@ -131,7 +146,7 @@ describe GeneratorPartials do
131
146
  header_includes,
132
147
  defns,
133
148
  c_module,
134
- true
149
+ include_variables: true, include_guard: nil
135
150
  )
136
151
 
137
152
  # Verify generate_source was called with correct parameters
@@ -208,7 +223,7 @@ describe GeneratorPartials do
208
223
  includes,
209
224
  decls,
210
225
  c_module,
211
- false
226
+ include_variables: false, include_guard: nil
212
227
  )
213
228
 
214
229
  # Verify file path utilities were called
@@ -447,6 +462,104 @@ describe GeneratorPartials do
447
462
 
448
463
  expect( buf.string ).to include( "struct Point { int x; int y; };\nstruct Color { int r; int g; int b; };\n" )
449
464
  end
465
+ # The spoof and the carried includes are what make this header compile wherever it lands.
466
+ # Order between them is load-bearing: a carried header can transitively reach the real
467
+ # module header, so the guard has to be satisfied before any include is processed.
468
+ it "states the module's own guard before the includes it carries, then the types" do
469
+ file_contents = <<~CONTENTS
470
+ #ifndef __CEEDLING_GENERATED_MY_MODULE_TYPES_H__
471
+ #define __CEEDLING_GENERATED_MY_MODULE_TYPES_H__
472
+
473
+ #define MY_MODULE_H
474
+
475
+ #include "foundation.h"
476
+ #include <stdint.h>
477
+
478
+ typedef uint8_t Byte;
479
+
480
+ #endif // __CEEDLING_GENERATED_MY_MODULE_TYPES_H__
481
+
482
+ CONTENTS
483
+
484
+ buf = types_buffer( 'my_module', 'my_module_types.h' )
485
+ typedef_stmt = CExtractorTypes::CStatement.new(text: "typedef uint8_t Byte;", line_num: 1)
486
+
487
+ @generator.generate_types(
488
+ name: 'my_module',
489
+ c_module: CExtractorTypes::CModule.new(
490
+ type_definitions: [typedef_stmt],
491
+ element_sequence: [typedef_stmt]
492
+ ),
493
+ output_path: '/path/to/output',
494
+ includes: [UserInclude.new('foundation.h'), SystemInclude.new('stdint.h')],
495
+ include_guard: 'MY_MODULE_H'
496
+ )
497
+
498
+ expect( buf.string.strip() ).to eq file_contents.strip()
499
+ end
500
+
501
+ it "states no guard when the module's header has none to spoof" do
502
+ buf = types_buffer( 'my_module', 'my_module_types.h' )
503
+ typedef_stmt = CExtractorTypes::CStatement.new(text: "typedef uint8_t Byte;", line_num: 1)
504
+
505
+ @generator.generate_types(
506
+ name: 'my_module',
507
+ c_module: CExtractorTypes::CModule.new(
508
+ type_definitions: [typedef_stmt],
509
+ element_sequence: [typedef_stmt]
510
+ ),
511
+ output_path: '/path/to/output',
512
+ includes: [UserInclude.new('foundation.h')],
513
+ include_guard: nil
514
+ )
515
+
516
+ expect( buf.string ).to include('#include "foundation.h"')
517
+ expect( buf.string.scan(/^#define /).length ).to eq(1) # the generated guard alone
518
+ end
519
+
520
+ # The real header's guard also arrives among the extracted macros, since it survives
521
+ # preprocessing. Stating it deliberately and carrying it too would duplicate it.
522
+ it "drops the carried copy of the guard it states itself" do
523
+ buf = types_buffer( 'my_module', 'my_module_types.h' )
524
+ guard_macro = CExtractorTypes::CStatement.new(text: "#define MY_MODULE_H", line_num: 1)
525
+ typedef_stmt = CExtractorTypes::CStatement.new(text: "typedef uint8_t Byte;", line_num: 2)
526
+
527
+ @generator.generate_types(
528
+ name: 'my_module',
529
+ c_module: CExtractorTypes::CModule.new(
530
+ type_definitions: [typedef_stmt],
531
+ macro_definitions: [guard_macro],
532
+ element_sequence: [guard_macro, typedef_stmt]
533
+ ),
534
+ output_path: '/path/to/output',
535
+ include_guard: 'MY_MODULE_H'
536
+ )
537
+
538
+ expect( buf.string.scan(/^#define MY_MODULE_H$/).length ).to eq(1)
539
+ end
540
+
541
+ # A macro whose name merely begins with the guard's, and a guard-named macro that actually
542
+ # carries a value. Neither is the guard statement.
543
+ it "keeps a macro whose name only resembles the guard" do
544
+ buf = types_buffer( 'my_module', 'my_module_types.h' )
545
+ near_macro = CExtractorTypes::CStatement.new(text: "#define MY_MODULE_H_LIMIT 8", line_num: 1)
546
+ valued_macro = CExtractorTypes::CStatement.new(text: "#define MY_MODULE_H 2", line_num: 2)
547
+ typedef_stmt = CExtractorTypes::CStatement.new(text: "typedef uint8_t Byte;", line_num: 3)
548
+
549
+ @generator.generate_types(
550
+ name: 'my_module',
551
+ c_module: CExtractorTypes::CModule.new(
552
+ type_definitions: [typedef_stmt],
553
+ macro_definitions: [near_macro, valued_macro],
554
+ element_sequence: [near_macro, valued_macro, typedef_stmt]
555
+ ),
556
+ output_path: '/path/to/output',
557
+ include_guard: 'MY_MODULE_H'
558
+ )
559
+
560
+ expect( buf.string ).to include('#define MY_MODULE_H_LIMIT 8')
561
+ expect( buf.string ).to include('#define MY_MODULE_H 2')
562
+ end
450
563
  end
451
564
 
452
565
  context "#generate_header (private method)" do
@@ -462,7 +575,7 @@ describe GeneratorPartials do
462
575
 
463
576
  CONTENTS
464
577
 
465
- @generator.send(:generate_header, buf, 'foo_bar', [], [], empty_module, false)
578
+ @generator.send(:generate_header, buf, 'foo_bar', [], [], empty_module, include_variables: false)
466
579
  expect( buf.string.strip() ).to eq file_contents.strip()
467
580
  end
468
581
 
@@ -483,7 +596,7 @@ describe GeneratorPartials do
483
596
  buf,
484
597
  'Apples-and-Bananas',
485
598
  [UserInclude.new('foo.h'), UserInclude.new('bar.h')],
486
- [], empty_module, false
599
+ [], empty_module, include_variables: false
487
600
  )
488
601
 
489
602
  expect( buf.string.strip() ).to eq file_contents.strip()
@@ -505,7 +618,7 @@ describe GeneratorPartials do
505
618
  make_var(name: 'slices_of_bread', type: 'unsigned int', text: 'unsigned int slices_of_bread = 10;'),
506
619
  make_var(name: 'crumbs', type: 'char', text: 'char crumbs[10];')
507
620
  )
508
- @generator.send(:generate_header, buf, 'pb-and-j', [], [], c_module, true)
621
+ @generator.send(:generate_header, buf, 'pb-and-j', [], [], c_module, include_variables: true)
509
622
  expect( buf.string.strip() ).to eq file_contents.strip()
510
623
  end
511
624
 
@@ -513,7 +626,7 @@ describe GeneratorPartials do
513
626
  c_module = make_module(
514
627
  make_var(name: 'cmd', type: 'cmd_t', text: 'cmd_t cmd = { 0 };', decorators: ['static', 'volatile'])
515
628
  )
516
- @generator.send(:generate_header, buf, 'mymod', [], [], c_module, true)
629
+ @generator.send(:generate_header, buf, 'mymod', [], [], c_module, include_variables: true)
517
630
 
518
631
  expect(buf.string).to include('extern volatile cmd_t cmd;')
519
632
  end
@@ -522,7 +635,7 @@ describe GeneratorPartials do
522
635
  c_module = make_module(
523
636
  make_var(name: 'lookup_table', type: 'uint8_t* const', text: 'uint8_t* const lookup_table;', decorators: ['static', 'const'])
524
637
  )
525
- @generator.send(:generate_header, buf, 'mymod', [], [], c_module, true)
638
+ @generator.send(:generate_header, buf, 'mymod', [], [], c_module, include_variables: true)
526
639
 
527
640
  expect(buf.string).to include('extern const uint8_t* const lookup_table;')
528
641
  end
@@ -576,7 +689,7 @@ describe GeneratorPartials do
576
689
  [UserInclude.new('Eeny.h'), UserInclude.new('Meeny.h')],
577
690
  decls,
578
691
  c_module,
579
- true
692
+ include_variables: true
580
693
  )
581
694
 
582
695
  expect( buf.string.strip() ).to eq file_contents.strip()
@@ -601,7 +714,7 @@ describe GeneratorPartials do
601
714
  make_stmt(text: "struct Point { int x; int y; };")
602
715
  )
603
716
 
604
- @generator.send(:generate_header, buf, 'defs', [], [], c_module, false)
717
+ @generator.send(:generate_header, buf, 'defs', [], [], c_module, include_variables: false)
605
718
  expect( buf.string.strip() ).to eq file_contents.strip()
606
719
  end
607
720
 
@@ -629,7 +742,7 @@ describe GeneratorPartials do
629
742
  make_stmt(text: "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)", line_num: 3)
630
743
  )
631
744
 
632
- @generator.send(:generate_header, buf, 'regs', [], [], c_module, false)
745
+ @generator.send(:generate_header, buf, 'regs', [], [], c_module, include_variables: false)
633
746
  expect( buf.string.strip() ).to eq file_contents.strip()
634
747
  end
635
748
 
@@ -644,7 +757,7 @@ describe GeneratorPartials do
644
757
  make_stmt(text: "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)", line_num: 3)
645
758
  )
646
759
 
647
- @generator.send(:generate_header, buf, 'regs', [], [], c_module, false)
760
+ @generator.send(:generate_header, buf, 'regs', [], [], c_module, include_variables: false)
648
761
 
649
762
  expect( buf.string ).to include(
650
763
  "#define START_ADDRESS 0x00 // don't change this\n" +
@@ -694,7 +807,7 @@ describe GeneratorPartials do
694
807
  element_sequence: [macro_stmt, typedef_stmt, var_decl, aggregate_stmt]
695
808
  )
696
809
 
697
- @generator.send(:generate_header, buf, 'all_statements', [], [], c_module, true)
810
+ @generator.send(:generate_header, buf, 'all_statements', [], [], c_module, include_variables: true)
698
811
  expect( buf.string.strip() ).to eq file_contents.strip()
699
812
  end
700
813
 
@@ -718,7 +831,7 @@ describe GeneratorPartials do
718
831
  make_stmt(text: "typedef uint8_t Byte;", line_num: 3)
719
832
  )
720
833
 
721
- @generator.send(:generate_header, buf, 'mixed', [], [], c_module, true)
834
+ @generator.send(:generate_header, buf, 'mixed', [], [], c_module, include_variables: true)
722
835
  expect( buf.string.strip() ).to eq file_contents.strip()
723
836
  end
724
837
 
@@ -738,7 +851,7 @@ describe GeneratorPartials do
738
851
  make_var( name: 'counter', type: 'int', text: 'int counter;', line_num: 2)
739
852
  )
740
853
 
741
- @generator.send(:generate_header, buf, 'interface', [], [], c_module, false)
854
+ @generator.send(:generate_header, buf, 'interface', [], [], c_module, include_variables: false)
742
855
  expect( buf.string.strip() ).to eq file_contents.strip()
743
856
  end
744
857
 
@@ -780,7 +893,7 @@ describe GeneratorPartials do
780
893
  element_sequence: [typedef_stmt, foo_raw, var_decl, bar_raw]
781
894
  )
782
895
 
783
- @generator.send(:generate_header, buf, 'interleaved', [], [foo_decl, bar_decl], c_module, true)
896
+ @generator.send(:generate_header, buf, 'interleaved', [], [foo_decl, bar_decl], c_module, include_variables: true)
784
897
  expect( buf.string.strip() ).to eq file_contents.strip()
785
898
  end
786
899
 
@@ -792,7 +905,7 @@ describe GeneratorPartials do
792
905
  raw = CExtractorTypes::CFunctionDeclaration.new(name: 'exported_func')
793
906
  c_module = make_module(raw)
794
907
 
795
- @generator.send(:generate_header, buf, 'mymod', [], [decl], c_module, false)
908
+ @generator.send(:generate_header, buf, 'mymod', [], [decl], c_module, include_variables: false)
796
909
 
797
910
  expect(buf.string).to include('__declspec(dllexport) void exported_func(void);')
798
911
  end
@@ -802,7 +915,7 @@ describe GeneratorPartials do
802
915
  make_var(name: 'counter', type: 'int', text: 'int counter __attribute__((aligned(16)));')
803
916
  )
804
917
 
805
- @generator.send(:generate_header, buf, 'mymod', [], [], c_module, true)
918
+ @generator.send(:generate_header, buf, 'mymod', [], [], c_module, include_variables: true)
806
919
 
807
920
  expect(buf.string).to include('extern int counter;')
808
921
  expect(buf.string).not_to include('__attribute__')
@@ -23,12 +23,18 @@ describe Partializer do
23
23
  @reportinator = Reportinator.new
24
24
  @loginator = double("Loginator").as_null_object
25
25
 
26
+ # Guard extraction reads the module's own header text through these two.
27
+ @preprocessinator_reconstructor = double("PreprocessinatorReconstructor")
28
+ @file_wrapper = double("FileWrapper")
29
+
26
30
  @partializer = described_class.new(
27
31
  {
28
32
  :partializer_helper => @partializer_helper,
29
33
  :file_finder => @file_finder,
30
34
  :c_extractor => @c_extractor,
31
35
  :file_path_utils => @file_path_utils,
36
+ :preprocessinator_reconstructor => @preprocessinator_reconstructor,
37
+ :file_wrapper => @file_wrapper,
32
38
  :reportinator => @reportinator,
33
39
  :loginator => @loginator
34
40
  }
@@ -809,6 +815,40 @@ describe Partializer do
809
815
  end
810
816
 
811
817
  context "#remap_implementation_source_includes" do
818
+ # A module tested and mocked in one test file splits its own content across two generated
819
+ # headers: the implementation carries the public bodies and the interface declares the
820
+ # private functions those bodies call. The generated source needs both, so this module's own
821
+ # interface header belongs in its own include list.
822
+ it "carries this module's own interface header when it is also mocked" do
823
+ allow(@file_path_utils).to receive(:form_partial_implementation_header_filename)
824
+ .with('module').and_return('ceedling_partial_module_impl.h')
825
+ allow(@file_path_utils).to receive(:form_partial_interface_header_filename)
826
+ .with('module').and_return('ceedling_partial_module_interface.h')
827
+
828
+ result = @partializer.remap_implementation_source_includes(
829
+ name: 'module',
830
+ includes: [UserInclude.new('module.h')],
831
+ partials: { 'module' => make_partial_config(tests_type: :public, mocks_type: :private) }
832
+ )
833
+
834
+ expect( result.map( &:filename ) ).to eq(
835
+ ['ceedling_partial_module_impl.h', 'ceedling_partial_module_interface.h']
836
+ )
837
+ end
838
+
839
+ it "carries no interface header for a module that is tested but not mocked" do
840
+ allow(@file_path_utils).to receive(:form_partial_implementation_header_filename)
841
+ .with('module').and_return('ceedling_partial_module_impl.h')
842
+
843
+ result = @partializer.remap_implementation_source_includes(
844
+ name: 'module',
845
+ includes: [UserInclude.new('module.h')],
846
+ partials: { 'module' => make_partial_config(tests_type: :public) }
847
+ )
848
+
849
+ expect( result.map( &:filename ) ).to eq(['ceedling_partial_module_impl.h'])
850
+ end
851
+
812
852
  it "returns implementation header when input is empty and no partials" do
813
853
  includes = []
814
854
  partials = {}
@@ -1214,6 +1254,129 @@ describe Partializer do
1214
1254
  ### extract_module_contents()
1215
1255
  ###
1216
1256
 
1257
+ context "#extract_module_include_guard" do
1258
+ # Read from the original header, not the reconstituted copy: that copy carries a synthetic
1259
+ # guard which #sanitize then strips, so it is not a dependable source for the name to spoof.
1260
+ it "reads a bounded prefix of the module's own header and returns the guard found" do
1261
+ expect(@file_wrapper).to receive(:read)
1262
+ .with('src/Foo.h', Partializer::GUARD_SCAN_BYTES)
1263
+ .and_return("#ifndef FOO_H\n#define FOO_H\n")
1264
+ allow(@preprocessinator_reconstructor).to receive(:extract_include_guard)
1265
+ .with("#ifndef FOO_H\n#define FOO_H\n")
1266
+ .and_return('FOO_H')
1267
+
1268
+ expect( @partializer.extract_module_include_guard('src/Foo.h') ).to eq('FOO_H')
1269
+ end
1270
+
1271
+ # A `#pragma once` header offers no macro any generated file could define, which is what a
1272
+ # nil return means to every caller.
1273
+ it "returns nil when the header carries no guard macro" do
1274
+ allow(@file_wrapper).to receive(:read).and_return("#pragma once\n")
1275
+ allow(@preprocessinator_reconstructor).to receive(:extract_include_guard).and_return(nil)
1276
+
1277
+ expect( @partializer.extract_module_include_guard('src/Foo.h') ).to be_nil
1278
+ end
1279
+
1280
+ it "returns nil for an absent filepath without reading anything" do
1281
+ expect(@file_wrapper).not_to receive(:read)
1282
+
1283
+ expect( @partializer.extract_module_include_guard(nil) ).to be_nil
1284
+ end
1285
+ end
1286
+
1287
+ context "#remap_types_header_includes" do
1288
+ # Nothing replaces this module's own header here. The types header cannot include itself, so
1289
+ # its own entry is simply dropped.
1290
+ it "drops the module's own header without substituting anything" do
1291
+ result = @partializer.remap_types_header_includes(
1292
+ name: 'module',
1293
+ includes: [UserInclude.new('module.h'), UserInclude.new('foundation.h')],
1294
+ partials: {}
1295
+ )
1296
+
1297
+ expect( result ).to eq([UserInclude.new('foundation.h')])
1298
+ end
1299
+
1300
+ it "keeps every other include in its original relative order" do
1301
+ result = @partializer.remap_types_header_includes(
1302
+ name: 'module',
1303
+ includes: [UserInclude.new('first.h'), UserInclude.new('module.h'), UserInclude.new('last.h')],
1304
+ partials: {}
1305
+ )
1306
+
1307
+ expect( result.map( &:filename ) ).to eq(['first.h', 'last.h'])
1308
+ end
1309
+
1310
+ # Another partialized module's real header would reintroduce exactly the content that
1311
+ # module's own Partial replaced, so a mocked one is redirected to its generated interface
1312
+ # header and an unmocked one is dropped.
1313
+ it "redirects a mocked partialized dependency to its interface header" do
1314
+ allow(@file_path_utils).to receive(:form_partial_interface_header_filename)
1315
+ .with('other')
1316
+ .and_return('ceedling_partial_other_interface.h')
1317
+
1318
+ result = @partializer.remap_types_header_includes(
1319
+ name: 'module',
1320
+ includes: [UserInclude.new('module.h'), UserInclude.new('other.h')],
1321
+ partials: { 'other' => make_partial_config(mocks_type: :public) }
1322
+ )
1323
+
1324
+ expect( result.map( &:filename ) ).to eq(['ceedling_partial_other_interface.h'])
1325
+ end
1326
+
1327
+ it "drops a partialized dependency that is not mocked" do
1328
+ result = @partializer.remap_types_header_includes(
1329
+ name: 'module',
1330
+ includes: [UserInclude.new('module.h'), UserInclude.new('other.h')],
1331
+ partials: { 'other' => make_partial_config(tests_type: :public) }
1332
+ )
1333
+
1334
+ expect( result ).to be_empty
1335
+ end
1336
+
1337
+ # Comparing a module name to an include means comparing names. A header whose basename
1338
+ # merely extends the module's is a different file.
1339
+ it "leaves a header whose name only extends a partialized module's name" do
1340
+ result = @partializer.remap_types_header_includes(
1341
+ name: 'module',
1342
+ includes: [UserInclude.new('module.h'), UserInclude.new('other_extra.h'), UserInclude.new('other.h')],
1343
+ partials: { 'other' => make_partial_config(tests_type: :public) }
1344
+ )
1345
+
1346
+ expect( result.map( &:filename ) ).to eq(['other_extra.h'])
1347
+ end
1348
+
1349
+ # The interface header includes the types header, so carrying it back into the types header
1350
+ # would fold that file's function declarations into this one.
1351
+ it "never carries this module's own interface header, even when it is also mocked" do
1352
+ allow(@file_path_utils).to receive(:form_partial_interface_header_filename)
1353
+ .and_return('ceedling_partial_module_interface.h')
1354
+
1355
+ result = @partializer.remap_types_header_includes(
1356
+ name: 'module',
1357
+ includes: [UserInclude.new('module.h'), UserInclude.new('foundation.h')],
1358
+ partials: { 'module' => make_partial_config(tests_type: :public, mocks_type: :private) }
1359
+ )
1360
+
1361
+ expect( result.map( &:filename ) ).to eq(['foundation.h'])
1362
+ end
1363
+
1364
+ it "logs the carried list under its own noun when a test is named" do
1365
+ expect(@loginator).to receive(:log_list).with(
1366
+ anything,
1367
+ 'Dependency includes to carry into the shared types header for Partial a_test::module:',
1368
+ Verbosity::OBNOXIOUS
1369
+ )
1370
+
1371
+ @partializer.remap_types_header_includes(
1372
+ name: 'module',
1373
+ includes: [UserInclude.new('foundation.h')],
1374
+ partials: {},
1375
+ test: 'a_test'
1376
+ )
1377
+ end
1378
+ end
1379
+
1217
1380
  context "#extract_module_contents" do
1218
1381
  before(:each) do
1219
1382
  @name = 'TestModule'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ceedling
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.9
4
+ version: 1.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark VanderVoord
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-09-20 00:00:00.000000000 Z
13
+ date: 2026-10-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -945,6 +945,7 @@ files:
945
945
  - spec/system/example_wondrous_forest_spec.rb
946
946
  - spec/system/gcov_deployment_spec.rb
947
947
  - spec/system/mixin_ordering_spec.rb
948
+ - spec/system/partials_types_header_spec.rb
948
949
  - spec/system/preprocessing_encoding_spec.rb
949
950
  - spec/system/preprocessing_fallback_conditionals_spec.rb
950
951
  - spec/system/preprocessing_locale_spec.rb
@@ -1799,6 +1800,7 @@ test_files:
1799
1800
  - spec/system/example_wondrous_forest_spec.rb
1800
1801
  - spec/system/gcov_deployment_spec.rb
1801
1802
  - spec/system/mixin_ordering_spec.rb
1803
+ - spec/system/partials_types_header_spec.rb
1802
1804
  - spec/system/preprocessing_encoding_spec.rb
1803
1805
  - spec/system/preprocessing_fallback_conditionals_spec.rb
1804
1806
  - spec/system/preprocessing_locale_spec.rb