Class: Autoproj::CLI::CI

Inherits:
InspectionTool
  • Object
show all
Defined in:
lib/autoproj/cli/ci.rb

Overview

Actual implementation of the functionality for the ‘autoproj ci` subcommand

Autoproj internally splits the CLI definition (Thor subclass) and the underlying functionality of each CLI subcommand. ‘autoproj-ci` follows the same pattern, and registers its subcommand in MainCI while implementing the functionality in this class

Defined Under Namespace

Classes: PullError

Constant Summary collapse

PHASES =
%w[import build test].freeze

Instance Method Summary collapse

Instance Method Details

#cache_pull(dir, ignore: []) ⇒ Object



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
# File 'lib/autoproj/cli/ci.rb', line 41

def cache_pull(dir, ignore: [])
    packages = resolve_packages

    memo = {}
    results = packages.each_with_object({}) do |pkg, h|
        if ignore.include?(pkg.name)
            pkg.message '%s: ignored by command line'
            fingerprint = pkg.fingerprint(memo: memo)
            h[pkg.name] = {
                'cached' => false,
                'fingerprint' => fingerprint
            }
            next
        end

        state, fingerprint,  =
            pull_package_from_cache(dir, pkg, memo: memo)
        if state
            pkg.message "%s: pulled #{fingerprint}", :green
        else
            pkg.message "%s: #{fingerprint} not in cache, "\
                        'or not pulled from cache'
        end

        h[pkg.name] = .merge(
            'cached' => state,
            'fingerprint' => fingerprint
        )
    end

    hit = results.count { |_, info| info['cached'] }
    Autoproj.message "#{hit} hits, #{results.size - hit} misses"

    results
end

#cache_push(dir) ⇒ Object



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
# File 'lib/autoproj/cli/ci.rb', line 77

def cache_push(dir)
    packages = resolve_packages
     = consolidated_report['packages']

    memo = {}
    results = packages.each_with_object({}) do |pkg, h|
        if !( = [pkg.name])
            pkg.message '%s: no metadata in build report', :magenta
            next
        elsif !(build_info = ['build'])
            pkg.message '%s: no build info in build report', :magenta
            next
        elsif build_info['cached']
            pkg.message '%s: was pulled from cache, not pushing'
            next
        elsif !build_info['success']
            pkg.message '%s: build failed, not pushing', :magenta
            next
        end

        # Remove cached flags before saving
         = .dup
        PHASES.each do |phase_name|
            [phase_name]&.delete('cached')
        end

        state, fingerprint = push_package_to_cache(
            dir, pkg, , force: true, memo: memo
        )
        if state
            pkg.message "%s: pushed #{fingerprint}", :green
        else
            pkg.message "%s: #{fingerprint} already in cache"
        end

        h[pkg.name] = {
            'updated' => state,
            'fingerprint' => fingerprint
        }
    end

    hit = results.count { |_, info| info['updated'] }
    Autoproj.message "#{hit} updated packages, #{results.size - hit} "\
                     'reused entries'

    results
end

#cache_state(dir, ignore: []) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/autoproj/cli/ci.rb', line 27

def cache_state(dir, ignore: [])
    packages = resolve_packages

    memo = {}
    packages.each_with_object({}) do |pkg, h|
        state = package_cache_state(dir, pkg, memo: memo)
        if ignore.include?(pkg.name)
            state = state.merge('cached' => false, 'metadata' => false)
        end

        h[pkg.name] = state
    end
end

#cleanup_build_cache(dir, size_limit) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/autoproj/cli/ci.rb', line 274

def cleanup_build_cache(dir, size_limit)
    all_files = Find.enum_for(:find, dir).map do |path|
        next unless File.file?(path) && File.file?("#{path}.json")

        [path, File.stat(path)]
    end.compact

    total_size = all_files.map { |_, s| s.size }.sum
    lru = all_files.sort_by { |_, s| s.mtime }

    while total_size > size_limit
        path, stat = lru.shift
        Autoproj.message "removing #{path} (size=#{stat.size}, mtime=#{stat.mtime})"

        FileUtils.rm_f path
        FileUtils.rm_f "#{path}.json"
        total_size -= stat.size
    end

    Autoproj.message format("current build cache size: %.1f GB", Float(total_size) / 1_000_000_000)
    total_size
end

#consolidated_reportObject



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
# File 'lib/autoproj/cli/ci.rb', line 314

def consolidated_report
    # NOTE: keys must match PHASES
    new_reports = {
        'import' => @ws.import_report_path,
        'build' => @ws.build_report_path,
        'test' => @ws.utility_report_path('test')
    }

    # We start with the cached info (if any) and override with
    # information from the other phase reports
    cache_report_path = File.join(@ws.root_dir, 'cache-pull.json')
    result = load_report(cache_report_path, 'cache_pull_report')['packages']
    result.delete_if do |_name, info|
        next true unless info.delete('cached')

        PHASES.each do |phase_name|
            if (phase_info = info[phase_name])
                phase_info['cached'] = true
            end
        end
        false
    end

    new_reports.each do |phase_name, path|
        report = load_report(path, "#{phase_name}_report")
        report['packages'].each do |pkg_name, pkg_info|
            result[pkg_name] ||= {}
            if pkg_info['invoked']
                result[pkg_name][phase_name] = pkg_info.merge(
                    'cached' => false,
                    'timestamp' => report['timestamp']
                )
            end
        end
    end
    { 'packages' => result }
end

#create_report(dir) ⇒ Object

Build a report in a given directory

The method itself will not archive the directory, only gather the information in a consistent way



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/autoproj/cli/ci.rb', line 176

def create_report(dir)
    initialize_and_load
    finalize_setup([], non_imported_packages: :ignore)

    report = consolidated_report
    FileUtils.mkdir_p(dir)
    File.open(File.join(dir, 'report.json'), 'w') do |io|
        JSON.dump(report, io)
    end

    installation_manifest = InstallationManifest
                            .from_workspace_root(@ws.root_dir)
    logs = File.join(dir, 'logs')

    # Pre-create the logs, or cp_r will have a different behavior
    # if the directory exists or not
    FileUtils.mkdir_p(logs)
    installation_manifest.each_package do |pkg|
        glob = Dir.glob(File.join(pkg.logdir, '*'))
        FileUtils.cp_r(glob, logs) if File.directory?(pkg.logdir)
    end
end

#load_built_flagsObject



297
298
299
300
301
302
303
304
305
306
# File 'lib/autoproj/cli/ci.rb', line 297

def load_built_flags
    path = @ws.build_report_path
    return {} unless File.file?(path)

    report = JSON.parse(File.read(path))
    report['build_report']['packages']
        .each_with_object({}) do |pkg_report, h|
            h[pkg_report['name']] = pkg_report['built']
        end
end

#load_report(path, root_name, default: { 'packages' => {} }) ⇒ Object



308
309
310
311
312
# File 'lib/autoproj/cli/ci.rb', line 308

def load_report(path, root_name, default: { 'packages' => {} })
    return default unless File.file?(path)

    JSON.parse(File.read(path)).fetch(root_name)
end

#need_xunit_processing?(results_dir, xunit_output, force: false) ⇒ Boolean

Checks if a package’s test results should be processed with xunit-viewer

Parameters:

  • results_dir (String)

    the directory where the

  • xunit_output (String)

    path to the xunit-viewer output. An existing file is re-generated only if force is true

  • force (Boolean) (defaults to: false)

    re-generation of the xunit-viewer output

Returns:

  • (Boolean)


131
132
133
134
135
136
137
138
139
140
141
# File 'lib/autoproj/cli/ci.rb', line 131

def need_xunit_processing?(results_dir, xunit_output, force: false)
    # We don't re-generate if the xunit-processed files were cached
    return if !force && File.file?(xunit_output)

    # We only check whether there are xml files in the
    # package's test dir. That's the only check we do ... if
    # the XML files are not JUnit, we'll finish with an empty
    # xunit html file
    Dir.enum_for(:glob, File.join(results_dir, '*.xml'))
       .first
end

#package_cache_path(dir, pkg, fingerprint: nil, memo: {}) ⇒ Object



199
200
201
202
# File 'lib/autoproj/cli/ci.rb', line 199

def package_cache_path(dir, pkg, fingerprint: nil, memo: {})
    fingerprint ||= pkg.fingerprint(memo: memo)
    File.join(dir, pkg.name, fingerprint)
end

#package_cache_state(dir, pkg, memo: {}) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/autoproj/cli/ci.rb', line 204

def package_cache_state(dir, pkg, memo: {})
    fingerprint = pkg.fingerprint(memo: memo)
    path = package_cache_path(dir, pkg, fingerprint: fingerprint, memo: memo)

    {
        'path' => path,
        'cached' => File.file?(path),
        'metadata' => File.file?("#{path}.json"),
        'fingerprint' => fingerprint
    }
end

#process_test_results(force: false, xunit_viewer: 'xunit-viewer') ⇒ Object

Post-processing of test results



168
169
170
# File 'lib/autoproj/cli/ci.rb', line 168

def process_test_results(force: false, xunit_viewer: 'xunit-viewer')
    process_test_results_xunit(force: force, xunit_viewer: xunit_viewer)
end

#process_test_results_xunit(force: false, xunit_viewer: 'xunit-viewer') ⇒ Object

Process the package’s test results with xunit-viewer

Parameters:

  • xunit_viewer (String) (defaults to: 'xunit-viewer')

    path to xunit-viewer

  • force (Boolean) (defaults to: false)

    re-generation of the xunit-viewer output. If false, packages that already have a xunit-viewer output will be skipped



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/autoproj/cli/ci.rb', line 148

def process_test_results_xunit(force: false, xunit_viewer: 'xunit-viewer')
    consolidated_report['packages'].each_value do |info|
        next unless info['test']
        next unless (results_dir = info['test']['target_dir'])

        xunit_output = "#{results_dir}.html"
        next unless need_xunit_processing?(results_dir, xunit_output,
                                           force: force)

        success = system(xunit_viewer,
                         "--results=#{results_dir}",
                         "--output=#{xunit_output}")
        unless success
            Autoproj.warn 'xunit-viewer conversion failed '\
                          "for '#{results_dir}'"
        end
    end
end

#pull_package_from_cache(dir, pkg, memo: {}) ⇒ Object



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
# File 'lib/autoproj/cli/ci.rb', line 219

def pull_package_from_cache(dir, pkg, memo: {})
    fingerprint = pkg.fingerprint(memo: memo)
    path = package_cache_path(dir, pkg, fingerprint: fingerprint, memo: memo)
    return [false, fingerprint, {}] unless File.file?(path)

     = "#{path}.json"
     =
        if File.file?()
            JSON.parse(File.read())
        else
            {}
        end

    # Do not pull packages for which we should run tests
    tests_enabled = pkg.test_utility.enabled?
    tests_invoked = ['test'] && ['test']['invoked']
    if tests_enabled && !tests_invoked
        pkg.message '%s: has tests that have never '\
                    'been invoked, not pulling from cache'
        return [false, fingerprint, {}]
    end

    FileUtils.mkdir_p(pkg.prefix)
    unless system('tar', 'xzf', path, chdir: pkg.prefix, out: '/dev/null')
        raise PullError, "tar failed when pulling cache file for #{pkg.name}"
    end

    [true, fingerprint, ]
end

#push_package_to_cache(dir, pkg, metadata, force: false, memo: {}) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/autoproj/cli/ci.rb', line 249

def push_package_to_cache(dir, pkg, , force: false, memo: {})
    fingerprint = pkg.fingerprint(memo: memo)
    path = package_cache_path(dir, pkg, fingerprint: fingerprint, memo: memo)
    temppath = "#{path}.#{Process.pid}.#{rand(256)}"

    FileUtils.mkdir_p(File.dirname(path))
    if force || !File.file?("#{path}.json")
        File.open(temppath, 'w') { |io| JSON.dump(, io) }
        FileUtils.mv(temppath, "#{path}.json")
    end

    if !force && File.file?(path)
        # Update modification time for the cleanup process
        FileUtils.touch(path)
        return [false, fingerprint]
    end

    result = system('tar', 'czf', temppath, '.',
                    chdir: pkg.prefix, out: '/dev/null')
    raise "tar failed when pushing cache file for #{pkg.name}" unless result

    FileUtils.mv(temppath, path)
    [true, fingerprint]
end

#resolve_packagesObject



17
18
19
20
21
22
23
24
25
# File 'lib/autoproj/cli/ci.rb', line 17

def resolve_packages
    initialize_and_load
    source_packages, * = finalize_setup(
        [], non_imported_packages: :ignore
    )
    source_packages.map do |pkg_name|
        ws.manifest.find_autobuild_package(pkg_name)
    end
end