File: tilt_template_test.rb

package info (click to toggle)
ruby-tilt 2.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 648 kB
  • sloc: ruby: 4,998; makefile: 7
file content (598 lines) | stat: -rw-r--r-- 19,726 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
require_relative 'test_helper'
require 'tempfile'
require 'tmpdir'
require 'pathname'

_MockTemplate = Class.new(Tilt::Template)

describe "tilt/template" do
  it "needs a file or block" do
    assert_raises(ArgumentError) { Tilt::Template.new }
  end

  it "handles frozen strings with :default_encoding" do
    inst = _MockTemplate.new('foo.erb', default_encoding: 'US-ASCII') {''.freeze}
    assert_equal false, inst.data.frozen?
    assert_equal Encoding::US_ASCII, inst.data.encoding
  end

  it "initializing with a file" do
    inst = _MockTemplate.new('foo.erb') {}
    assert_equal 'foo.erb', inst.file
  end

  it "initializing with a file and line" do
    inst = _MockTemplate.new('foo.erb', 55) {}
    assert_equal 'foo.erb', inst.file
    assert_equal 55, inst.line
  end

  it "initializing with a tempfile" do
    tempfile = Tempfile.new('tilt_template_test')
    inst = _MockTemplate.new(tempfile)
    assert_equal File.basename(tempfile.path), inst.basename
  end

  it "initializing with an object responding to to_path" do
    o = Object.new
    def o.to_path; 'foo.erb' end
    assert_equal 'foo.erb', _MockTemplate.new(o){}.file
  end

  it "initializing with an object not responding to any expected convertor" do
    assert_raises(TypeError) do
      _MockTemplate.new(Object.new)
    end
  end

  it "initializing with a pathname" do
    tempfile = Tempfile.new('tilt_template_test')
    pathname = Pathname.new(tempfile.path)
    inst = _MockTemplate.new(pathname)
    assert_equal File.basename(tempfile.path), inst.basename
  end

  it "initialize with hash that implements #path and #to_path" do
    _SillyHash = Class.new(Hash) do
      def path; end
      def to_path; end
    end

    options = _SillyHash[:key => :value]
    inst = _MockTemplate.new(options) {}
    assert_equal :value, inst.options[:key]
  end

  it "uses correct eval_file" do
    inst = _MockTemplate.new('foo.erb', 55) {}
    assert_equal 'foo.erb', inst.eval_file
  end

  it "uses a default filename for #eval_file when no file provided" do
    inst = _MockTemplate.new { 'Hi' }
    refute_nil inst.eval_file
    assert !inst.eval_file.include?("\n")
  end

  it "calculating template's #basename" do
    inst = _MockTemplate.new('/tmp/templates/foo.html.erb') {}
    assert_equal 'foo.html.erb', inst.basename
  end

  it "calculating the template's #name" do
    inst = _MockTemplate.new('/tmp/templates/foo.html.erb') {}
    assert_equal 'foo', inst.name
  end

  it "handles #basename and #name without file" do
    inst = _MockTemplate.new {}
    assert_nil inst.basename
    assert_nil inst.name
  end

  it "#metadata returns class metdata" do
    assert_equal({}, _MockTemplate.new{}.metadata)
  end

  it "initializing with a data loading block" do
    _MockTemplate.new { |template| "Hello World!" }
  end

  _PreparingMockTemplate = Class.new(Tilt::Template) do
    def prepare
      raise "data must be set" if data.nil?
      @prepared = true
    end
    def prepared? ; @prepared ; end
  end

  it "raises NotImplementedError when #evaluate or #template_source not defined" do
    inst = _PreparingMockTemplate.new { |t| "Hello World!" }
    assert_raises(NotImplementedError) { inst.render }
    assert inst.prepared?
  end

  _SimpleMockTemplate = Class.new(_PreparingMockTemplate) do
    def evaluate(scope, locals, &block)
      raise "should be prepared" unless prepared?
      raise "scope should be present" if scope.nil?
      raise "locals should be present" if locals.nil?
      "<em>#{@data}</em>"
    end
  end

  it "prepares and evaluates the template on #render" do
    inst = _SimpleMockTemplate.new { |t| "Hello World!" }
    assert_equal "<em>Hello World!</em>", inst.render
    assert inst.prepared?
  end

  it 'prepares and evaluates the template on #render with nil arg' do
    inst = _SimpleMockTemplate.new { |t| "Hello World!" }
    assert_equal '<em>Hello World!</em>', inst.render(nil)
    assert inst.prepared?
  end

  it "#fixed_locals? returns whether the template uses fixed locals" do
    assert_equal false, _MockTemplate.new{}.fixed_locals?
    assert_equal true, _MockTemplate.new(fixed_locals: "()"){}.fixed_locals?
  end

  _SourceGeneratingMockTemplate = Class.new(_PreparingMockTemplate) do
    def precompiled_template(locals)
      "foo = [] ; foo << %Q{#{data}} ; foo.join"
    end
  end

  _FrozenSourceGeneratingMockTemplate = Class.new(_SourceGeneratingMockTemplate) do
    def freeze_string_literals?
      true
    end
  end

  it "template_source with locals" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{name}!' }
    assert_equal "Hey Joe!", inst.render(Object.new, :name => 'Joe')
    assert inst.prepared?
  end

  it "template_source with locals of strings" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{name}!' }
    assert_equal "Hey Joe!", inst.render(Object.new, 'name' => 'Joe')
    assert inst.prepared?
  end

  it "template_source with locals of strings" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{name}!' }
    assert_equal "Hey Joe!", inst.render(Object.new, 'name' => 'Joe', :name=>'Joe')
    assert inst.prepared?
  end

  it "template_source with locals having non-variable keys raises error" do
    inst = _SourceGeneratingMockTemplate.new { |t| '1 + 2 = #{_answer}' }
    err = assert_raises(RuntimeError) { inst.render(Object.new, 'ANSWER' => 3) }
    assert_equal "invalid locals key: \"ANSWER\" (keys must be variable names)", err.message
    assert_equal "1 + 2 = 3", inst.render(Object.new, '_answer' => 3)
  end

  it "#compiled_method should return UnboundMethod for given local keys and scope class" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey' }
    m = inst.compiled_method([], Object)
    assert_kind_of UnboundMethod, m
    o = Object.new
    o.define_singleton_method(:x, m)
    assert_equal 'Hey', o.x({})
    assert_same m, inst.compiled_method([], Object)
    m1 = inst.compiled_method([:a], Object)
    m2 = inst.compiled_method([], Array)
    refute_same m, m1
    refute_same m, m2
    o.define_singleton_method(:y, m1)
    assert_equal 'Hey', o.y(a: 1)
    ary = []
    ary.define_singleton_method(:z, m2)
    assert_equal 'Hey', ary.z({})
  end

  it "template_source with nil locals" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey' }
    assert_equal 'Hey', inst.render(Object.new, nil)
    assert inst.prepared?
  end

  it "template_source with locals including 'locals'" do
    # Skip in CI on JRuby 9.1/9.2, as CI fails even though tests pass locally with these
    # JRuby versions.
    skip if defined?(JRUBY_VERSION) && JRUBY_VERSION < '9.3' && ENV['COFFEE_SCRIPT'] == 'use'

    # Ensure that a locals hash value named `locals` doesn't clobber the ability to assign other
    # locals that follow it in sorted order
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{name}!' }
    assert_equal "Hey Jane!", inst.render(Object.new, :locals => [], :name => 'Jane')
    assert inst.prepared?
  end

  {:method=>"compiled_path= method", :option=>":compiled_path option"}.each do |check_type, desc|
    if check_type == :option
      with_compiled_path = lambda do |path, &block|
        _SourceGeneratingMockTemplate.new(compiled_path: path, &block)
      end
    else
      with_compiled_path = lambda do |path, &block|
        inst = _SourceGeneratingMockTemplate.new(&block)
        inst.compiled_path = path
        inst
      end
    end

    it "template without #{desc} " do
      inst = with_compiled_path.(nil) { |t| 'Hey' }
      assert_nil inst.compiled_path
    end

    it "template with #{desc}" do
      Dir.mktmpdir('tilt') do |dir|
        base = File.join(dir, 'template')
        inst = with_compiled_path.(base) { |t| 'Hey' }

        tempfile = "#{base}.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey', inst.render
        assert_equal true, File.file?(tempfile)
        assert_match(/\Aclass Object/, File.read(tempfile))

        tempfile = "#{base}-1.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey', inst.render("")
        assert_equal true, File.file?(tempfile)
        assert_match(/\Aclass String/, File.read(tempfile))

        tempfile = "#{base}-2.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey', inst.render(Tilt::Mapping.new)
        assert_equal true, File.file?(tempfile)
        assert_match(/\Aclass Tilt::Mapping/, File.read(tempfile))
      end
    end

    it "template with #{desc} and with anonymous scope_class" do
      Dir.mktmpdir('tilt') do |dir|
        base = File.join(dir, 'template')
        inst = with_compiled_path.(base) { |t| 'Hey' }

        message = nil
        inst.define_singleton_method(:warn) { |msg| message = msg }
        scope_class = Class.new
        assert_equal 'Hey', inst.render(scope_class.new)
        assert_equal "compiled_path (#{base.inspect}) ignored on template with anonymous scope_class (#{scope_class.inspect})", message
        assert_equal [], Dir["#{dir}/*"]
      end
    end

    it "template with #{desc} and with locals" do
      Dir.mktmpdir('tilt') do |dir|
        base = File.join(dir, 'template')
        inst = with_compiled_path.(base + '.rb') { |t| 'Hey #{defined?(a)} #{defined?(b)}' }

        tempfile = "#{base}.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey local-variable ', inst.render(Object.new, 'a' => 1)
        content = File.read(tempfile)
        assert_match(/\Aclass Object/, content)
        assert_includes(content, "\na = locals[\"a\"]\n")

        tempfile = "#{base}-1.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey local-variable local-variable', inst.render(Object.new, 'b' => 1, 'a' => 1)
        content = File.read(tempfile)
        assert_match(/\Aclass Object/, content)
        assert_includes(content, "\na = locals[\"a\"]\nb = locals[\"b\"]\n")
      end
    end

    it "template with compiled_path and freezing string literals option" do
      Dir.mktmpdir('tilt') do |dir|
        base = File.join(dir, 'template')
        if check_type == :option
          inst = _FrozenSourceGeneratingMockTemplate.new(compiled_path: base) { |t| 'Hey' }
        else
          inst = _FrozenSourceGeneratingMockTemplate.new { |t| 'Hey' }
          inst.compiled_path = base
        end

        tempfile = "#{base}.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey', inst.render
        assert_equal true, File.file?(tempfile)
        assert_match(/\A# frozen-string-literal: true\nclass Object/, File.read(tempfile))

        tempfile = "#{base}-1.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey', inst.render("")
        assert_equal true, File.file?(tempfile)
        assert_match(/\A# frozen-string-literal: true\nclass String/, File.read(tempfile))

        tempfile = "#{base}-2.rb"
        assert_equal false, File.file?(tempfile)
        assert_equal 'Hey', inst.render(Tilt::Mapping.new)
        assert_equal true, File.file?(tempfile)
        assert_match(/\A# frozen-string-literal: true\nclass Tilt::Mapping/, File.read(tempfile))
      end
    end
  end

  it "template with :compiled_path option and with :scope_class and fixed locals" do
    Dir.mktmpdir('tilt') do |dir|
      base = File.join(dir, 'template')
      tempfile = "#{base}.rb"
      assert_equal false, File.file?(tempfile)
      inst =  _SourceGeneratingMockTemplate.new(compiled_path: base + '.rb', scope_class: Array, fixed_locals: '(a: nil, b: nil)') { |t| 'Hey #{defined?(a)} #{defined?(b)}' }

      assert_equal true, File.file?(tempfile)
      assert_equal 'Hey local-variable local-variable', inst.render(Object.new)
      content = File.read(tempfile)
      assert_match(/\Aclass Array/, content)
      assert_includes(content, "(a: nil, b: nil)")

      assert_equal 'Hey local-variable local-variable', inst.render(Object.new, :a => 1)
      assert_equal content, File.read(tempfile)

      assert_equal 'Hey local-variable local-variable', inst.render(Object.new, :b => 1, :a => 1)
      assert_equal content, File.read(tempfile)

      assert_equal false, File.file?("#{base}-1.rb")
    end
  end

  _CustomGeneratingMockTemplate = Class.new(_PreparingMockTemplate) do
    def precompiled_template(locals)
      data
    end

    def precompiled_preamble(locals)
      options.fetch(:preamble)
    end

    def precompiled_postamble(locals)
      options.fetch(:postamble)
    end
  end

  it "supports pre/postamble" do
    inst = _CustomGeneratingMockTemplate.new(
      :preamble => 'buf = []',
      :postamble => 'buf.join'
    ) { 'buf << 1' }

    assert_equal "1", inst.render
  end

  _Person = Class.new do
    self::CONSTANT = "Bob"

    attr_accessor :name
    def initialize(name)
      @name = name
    end
  end

  it "template_source with an object scope" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{@name}!' }
    scope = _Person.new('Joe')
    assert_equal "Hey Joe!", inst.render(scope)
  end

  it "template_source with a block for yield" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{yield}!' }
    assert_equal "Hey Joe!", inst.render(Object.new){ 'Joe' }
  end

  it "template which accesses a constant" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT}!' }
    assert_equal "Hey Bob!", inst.render(_Person.new("Joe"))
  end

  it "supports :scope_class option" do
    inst = _SourceGeneratingMockTemplate.new(scope_class: _Person) { |t| 'Hey #{CONSTANT}!' }
    assert_equal "Hey Bob!", inst.render
  end

  it "supports :scope_class and fixed_locals options provided together" do
    inst = _SourceGeneratingMockTemplate.new(scope_class: _Person, fixed_locals: "()") { |t| 'Hey #{CONSTANT}!' }
    assert_equal "Hey Bob!", inst.render
  end

  it "template which accesses a constant using scope class" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT}!' }
    assert_equal "Hey Bob!", inst.render(_Person)
  end

  _BasicPerson = Class.new(BasicObject) do
    self::CONSTANT = "Bob"

    attr_accessor :name
    def initialize(name)
      @name = name
    end
  end

  it "template_source with an BasicObject scope" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{@name}!' }
    scope = _BasicPerson.new('Joe')
    assert_equal "Hey Joe!", inst.render(scope)
  end

  it "template_source with a block for yield using BasicObject instance" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{yield}!' }
    assert_equal "Hey Joe!", inst.render(BasicObject.new){ 'Joe' }
  end

  it "template which accesses a BasicObject constant" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT}!' }
    assert_equal "Hey Bob!", inst.render(_BasicPerson.new("Joe"))
  end

  it "template which accesses a constant using BasicObject scope class" do
    inst = _SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT}!' }
    assert_equal "Hey Bob!", inst.render(_BasicPerson)
  end

  if RUBY_VERSION >= '2.3'
    _FrozenStringMockTemplate = Class.new(_PreparingMockTemplate) do
      def freeze_string_literals?
        true
      end
      def precompiled_template(locals)
        "'bar'"
      end
    end

    it "uses frozen literal strings if freeze_literal_strings? is true" do
      inst = _FrozenStringMockTemplate.new{|d| 'a'}
      assert_equal "bar", inst.render
      assert_equal true, inst.render.frozen?
      assert inst.prepared?
    end
  end
end

  ##
  # Encodings
describe "tilt/template (encoding)" do
  _DynamicMockTemplate = Class.new(_MockTemplate) do
    def precompiled_template(locals)
      options[:code]
    end
  end

  _UTF8Template = Class.new(_MockTemplate) do
    def default_encoding
      Encoding::UTF_8
    end
  end

  before do
    @file = Tempfile.open('template')
    @file.puts "stuff"
    @file.close
    @template = @file.path
  end

  after do
    @file.delete
  end

  it "reading from file assumes default external encoding" do
    with_default_encoding('Big5') do
      inst = _MockTemplate.new(@template)
      assert_equal 'Big5', inst.data.encoding.to_s
    end
  end

  it "reading from file with a :default_encoding overrides default external" do
    with_default_encoding('Big5') do
      inst = _MockTemplate.new(@template, :default_encoding => 'GBK')
      assert_equal 'GBK', inst.data.encoding.to_s
    end
  end

  it "reading from file with default_internal set does no transcoding" do
    begin
      silence{Encoding.default_internal = 'utf-8'}
      with_default_encoding('Big5') do
        inst = _MockTemplate.new(@template)
        assert_equal 'Big5', inst.data.encoding.to_s
      end
    ensure
      silence{Encoding.default_internal = nil}
    end
  end

  it "using provided template data verbatim when given as string" do
    with_default_encoding('Big5') do
      inst = _MockTemplate.new(@template) { "blah".dup.force_encoding('GBK') }
      assert_equal 'GBK', inst.data.encoding.to_s
    end
  end

  it "uses the template from the generated source code" do
    with_utf8_default_encoding do
      tmpl = "ふが"
      code = tmpl.inspect.encode('Shift_JIS')
      inst = _DynamicMockTemplate.new(:code => code) { '' }
      res = inst.render
      assert_equal 'Shift_JIS', res.encoding.to_s
      assert_equal tmpl, res.encode(tmpl.encoding)
    end
  end

  it "uses the magic comment from the generated source code when generated source code is frozen" do
    with_utf8_default_encoding do
      tmpl = "ふが"
      code = ("# coding: Shift_JIS\n" + tmpl.inspect).encode('Shift_JIS')
      # Set it to an incorrect encoding
      code.force_encoding('UTF-8')
      code.freeze

      inst = _DynamicMockTemplate.new(:code => code) { '' }
      res = inst.render
      assert_equal 'Shift_JIS', res.encoding.to_s
      assert_equal tmpl, res.encode(tmpl.encoding)
    end
  end

  it "uses the magic comment from the generated source code" do
    with_utf8_default_encoding do
      tmpl = "ふが"
      code = ("# coding: Shift_JIS\n" + tmpl.inspect).encode('Shift_JIS')
      # Set it to an incorrect encoding
      code.force_encoding('UTF-8')

      inst = _DynamicMockTemplate.new(:code => code) { '' }
      res = inst.render
      assert_equal 'Shift_JIS', res.encoding.to_s
      assert_equal tmpl, res.encode(tmpl.encoding)
    end
  end

  it "uses compiled template encoding if :skip_compiled_encoding_detection is true" do
    with_utf8_default_encoding do
      tmpl = 'x'
      code = ("# coding: UTF-8\n" + tmpl.inspect).encode('UTF-8')
      # Set it to an incorrect encoding
      code.force_encoding('US-ASCII')

      inst = _DynamicMockTemplate.new(:code => code, :skip_compiled_encoding_detection=>true) { '' }
      res = inst.render
      assert_equal 'US-ASCII', res.encoding.to_s
      assert_equal tmpl, res.encode(tmpl.encoding)
    end
  end

  it "uses #default_encoding instead of default_external" do
    with_default_encoding('Big5') do
      inst = _UTF8Template.new(@template)
      assert_equal 'UTF-8', inst.data.encoding.to_s
    end
  end

  it "uses #default_encoding instead of current encoding" do
    tmpl = "".dup.force_encoding('Big5')
    inst = _UTF8Template.new(@template) { tmpl }
    assert_equal 'UTF-8', inst.data.encoding.to_s
  end

  it "raises error if the encoding is not valid" do
    assert_raises(Encoding::InvalidByteSequenceError) do
      _UTF8Template.new(@template) { "\xe4" }
    end
  end

  it "StaticTemplate#compiled_method raise NotImplementedError" do
    c = Tilt::StaticTemplate.subclass{data}
    t = c.new{''}
    assert_raises(NotImplementedError) do
      t.compiled_method([], Object)
    end
  end
end