Skip to content

Commit

Permalink
* Refactor loginator for background processing
Browse files Browse the repository at this point in the history
* Refactor batchinator for better handling of exceptions
* Refactor test build to directly drive builds in parallel again (not rake invoked)
  • Loading branch information
mvandervoord committed Oct 14, 2024
1 parent 48f45a0 commit c387969
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 66 deletions.
58 changes: 33 additions & 25 deletions lib/ceedling/build_batchinator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,39 +52,47 @@ def exec(workload:, things:, &block)

threads = (1..workers).collect do
thread = Thread.new do
begin
# Run tasks until there are no more enqueued
loop do
# pop(true) is non-blocking and raises ThreadError when queue is empty
yield @queue.pop(true)
end

# First, handle thread exceptions (should always be due to empty queue)
rescue ThreadError => e
# Typical case: do nothing and allow thread to wind down
Thread.handle_interrupt(Exception => :never) do
begin
Thread.handle_interrupt(Exception => :immediate) do
# Run tasks until there are no more enqueued
loop do
# pop(true) is non-blocking and raises ThreadError when queue is empty
yield @queue.pop(true)
end
end
# First, handle thread exceptions (should always be due to empty queue)
rescue ThreadError => e
# Typical case: do nothing and allow thread to wind down

# ThreadError outside scope of expected empty queue condition
unless e.message.strip.casecmp("queue empty")
@loginator.log(e.message, Verbosity::ERRORS)

# Shutdown all worker threads
shutdown_threads(threads) #TODO IT SEEMS LIKE threads MIGHT NOT BE VALID YET

raise(e) # Raise exception again
end

# Second, catch every other kind of exception so we can intervene with thread cleanup.
# Generally speaking, catching Exception is a no-no, but we must in this case.
# Raise the exception again so that:
# 1. Calling code knows something bad happened and handles appropriately
# 2. Ruby runtime can handle most serious problems
rescue Exception => e
@loginator.log(e.message, Verbosity::ERRORS)

# ThreadError outside scope of expected empty queue condition
unless e.message.strip.casecmp("queue empty")
# Shutdown all worker threads
shutdown_threads(threads)
shutdown_threads(threads) #TODO IT SEEMS LIKE threads MIGHT NOT BE VALID YET

raise(e) # Raise exception again
raise(e) # Raise exception again after intervening
end

# Second, catch every other kind of exception so we can intervene with thread cleanup.
# Generally speaking, catching Exception is a no-no, but we must in this case.
# Raise the exception again so that:
# 1. Calling code knows something bad happened and handles appropriately
# 2. Ruby runtime can handle most serious problems
rescue Exception => e
# Shutdown all worker threads
shutdown_threads(threads)

raise(e) # Raise exception again after intervening
end
end

# Hand thread to Enumerable collect() routine
thread.abort_on_exception = true
thread
end

Expand Down
89 changes: 68 additions & 21 deletions lib/ceedling/loginator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,68 @@ def setup()

@project_logging = false
@log_filepath = nil

@queue = Queue.new
@worker = Thread.new do
# Run tasks until there are no more enqueued
@done = false
while !@done do
Thread.handle_interrupt(Exception => :never) do
begin
Thread.handle_interrupt(Exception => :immediate) do
# pop(false) is blocking and should just hang here and wait for next message
item = @queue.pop(false)
if (item.nil?)
@done = true
next
end

# pick out the details
message = item[:message]
label = item[:label]
verbosity = item[:verbosity]
stream = item[:stream]

# Write to log as though Verbosity::DEBUG (no filtering at all) but without fun characters
if @project_logging
file_msg = message.dup() # Copy for safe inline modifications

# Add labels
file_msg = format( file_msg, verbosity, label, false )

# Note: In practice, file-based logging only works with trailing newlines (i.e. `log()` calls)
# `out()` calls will be a little ugly in the log file, but these are typically only
# used for console logging anyhow.
logfile( sanitize( file_msg, false ), extract_stream_name( stream ) )
end

# Only output to console when message reaches current verbosity level
if !stream.nil? && (@verbosinator.should_output?( verbosity ))
# Add labels and fun characters
console_msg = format( message, verbosity, label, @decorators )

# Write to output stream after optionally removing any problematic characters
stream.print( sanitize( console_msg, @decorators ) )
end
end
rescue ThreadError
@done = true
rescue Exception => e
puts e.inspect
end
end
end
end
end

def wrapup
begin
@queue.close
@worker.join
rescue
#If we failed at this point, just give up on it
end
end

def set_logfile( log_filepath )
if !log_filepath.empty?
Expand Down Expand Up @@ -89,27 +149,14 @@ def log(message="\n", verbosity=Verbosity::NORMAL, label=LogLabels::AUTO, stream
# Message contatenated with "\n" (unless it aready ends with a newline)
message += "\n" unless message.end_with?( "\n" )

# Write to log as though Verbosity::DEBUG (no filtering at all) but without fun characters
if @project_logging
file_msg = message.dup() # Copy for safe inline modifications

# Add labels
file_msg = format( file_msg, verbosity, label, false )

# Note: In practice, file-based logging only works with trailing newlines (i.e. `log()` calls)
# `out()` calls will be a little ugly in the log file, but these are typically only
# used for console logging anyhow.
logfile( sanitize( file_msg, false ), extract_stream_name( stream ) )
end

# Only output to console when message reaches current verbosity level
return if !(@verbosinator.should_output?( verbosity ))

# Add labels and fun characters
console_msg = format( message, verbosity, label, @decorators )

# Write to output stream after optionally removing any problematic characters
stream.print( sanitize( console_msg, @decorators ) )
# Add item to the queue
item = {
:message => message,
:verbosity => verbosity,
:label => label,
:stream => stream
}
@queue << item
end


Expand Down
1 change: 1 addition & 0 deletions lib/ceedling/objects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ test_invoker:
- generator
- test_context_extractor
- file_path_utils
- file_finder
- file_wrapper
- verbosinator

Expand Down
3 changes: 3 additions & 0 deletions lib/ceedling/rakefile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ def test_failures_handler()
ops_done = SystemWrapper.time_stopwatch_s()
log_runtime( 'operations', start_time, ops_done, CEEDLING_APPCFG.build_tasks? )
boom_handler( @ceedling[:loginator], ex )
@ceedling[:loginator].wrapup
exit(1)
end

@ceedling[:loginator].wrapup
exit(0)
else
msg = "Ceedling could not complete operations because of errors"
Expand All @@ -136,6 +138,7 @@ def test_failures_handler()
rescue => ex
boom_handler( @ceedling[:loginator], ex)
ensure
@ceedling[:loginator].wrapup
exit(1)
end
end
Expand Down
11 changes: 7 additions & 4 deletions lib/ceedling/test_invoker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class TestInvoker
:test_context_extractor,
:file_path_utils,
:file_wrapper,
:file_finder,
:verbosinator

def setup
Expand Down Expand Up @@ -348,16 +349,18 @@ def setup_and_invoke(tests:, context:TEST_SYM, options:{})
details[:no_link_objects] = test_no_link_objects
details[:results_pass] = test_pass
details[:results_fail] = test_fail
details[:tool] = TOOLS_TEST_COMPILER #TODO: VERIFY THIS GETS REPLACED WHEN IN GCOV OR BULLSEYE MODE
end
end
end

# Build All Test objects
@batchinator.build_step("Building Objects") do
# FYI: Temporarily removed direct object generation to allow rake invoke() to execute custom compilations (plugins, special cases)
# @test_invoker_helper.generate_objects_now(object_list, options)
@testables.each do |_, details|
@task_invoker.invoke_test_objects(test: details[:name], objects:details[:objects])
details[:objects].each do |obj|
src = @file_finder.find_build_input_file(filepath: obj, context: TEST_SYM)
compile_test_component(tool: details[:tool], context: TEST_SYM, test: details[:name], source: src, object: obj, msg: details[:msg])
end
end
end

Expand Down Expand Up @@ -432,7 +435,7 @@ def lookup_sources(test:)
end

def compile_test_component(tool:, context:TEST_SYM, test:, source:, object:, msg:nil)
testable = @testables[test]
testable = @testables[test.to_sym]
filepath = testable[:filepath]
defines = testable[:compile_defines]

Expand Down
21 changes: 5 additions & 16 deletions plugins/gcov/lib/gcov.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,17 @@ def automatic_reporting_enabled?
return (@project_config[:gcov_report_task] == false)
end

def generate_coverage_object_file(test, source, object)
# Non-coverage compiler
tool = TOOLS_TEST_COMPILER
msg = nil
def pre_compile_execute(arg_hash)
source = arg_hash[:source]

# Handle assembly file that comes through
if File.extname(source) == EXTENSION_ASSEMBLY
tool = TOOLS_TEST_ASSEMBLER
arg_hash[:tool] = TOOLS_TEST_ASSEMBLER
# If a source file (not unity, mocks, etc.) is to be compiled use code coverage compiler
elsif @configurator.collection_all_source.include?(source)
tool = TOOLS_GCOV_COMPILER
msg = "Compiling #{File.basename(source)} with coverage..."
arg_hash[:tool] = TOOLS_GCOV_COMPILER
arg_hash[:msg] = "Compiling #{File.basename(source)} with coverage..."
end

@test_invoker.compile_test_component(
tool: tool,
context: GCOV_SYM,
test: test,
source: source,
object: object,
msg: msg
)
end

# `Plugin` build step hook
Expand Down

0 comments on commit c387969

Please sign in to comment.