diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index fd1d63b..05ed31a 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -9,12 +9,38 @@ name: unit jobs: test: - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos] - ruby: ['3.1', '3.4'] - name: Unit test on Ruby ${{ matrix.os }} with Ruby ${{ matrix.ruby }} + include: + # Intel runners + - os: macos-13 + ruby: '3.1' + architecture: 'Intel' + - os: macos-13 + ruby: '3.4' + architecture: 'Intel' + - os: macos-15-intel + ruby: '3.1' + architecture: 'Intel' + - os: macos-15-intel + ruby: '3.4' + architecture: 'Intel' + # Apple Silicon runners - oldest (macos-14) + - os: macos-14 + ruby: '3.1' + architecture: 'Apple Silicon' + - os: macos-14 + ruby: '3.4' + architecture: 'Apple Silicon' + # Apple Silicon runners - newest (macos-15) + - os: macos-15 + ruby: '3.1' + architecture: 'Apple Silicon' + - os: macos-15 + ruby: '3.4' + architecture: 'Apple Silicon' + name: Unit test on ${{ matrix.os }} (${{ matrix.architecture }}) with Ruby ${{ matrix.ruby }} steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 diff --git a/spec/array_spec.rb b/spec/array_spec.rb index a7cb059..4692ae4 100644 --- a/spec/array_spec.rb +++ b/spec/array_spec.rb @@ -98,4 +98,149 @@ expect(values).to eq({ 0 => CF::Boolean::TRUE, 1 => CF::String.from_string('123') }) end end + + # Test to simulate the crash that occurs in Chef's macos_userdefaults resource + # when processing array values on Intel macOS + describe 'crash simulation for array processing on Intel macOS' do + context 'with an array of file paths similar to Chef macos_userdefaults' do + let(:file_paths) do + [ + "/Library/Managed Installs/fake.log", + "/Library/Managed Installs/also_fake.log" + ] + end + + it 'should successfully create an immutable array from string paths' do + cf_strings = file_paths.map { |path| CF::String.from_string(path) } + expect { CF::Array.immutable(cf_strings) }.not_to raise_error + end + + it 'should successfully iterate over an array of file paths without segfault' do + cf_strings = file_paths.map { |path| CF::String.from_string(path) } + array = CF::Array.immutable(cf_strings) + + collected_values = [] + expect { + array.each do |value| + collected_values << value + end + }.not_to raise_error + + expect(collected_values.length).to eq(2) + expect(collected_values[0]).to be_a(CF::String) + expect(collected_values[1]).to be_a(CF::String) + end + + it 'should successfully convert array to ruby without segfault' do + cf_strings = file_paths.map { |path| CF::String.from_string(path) } + array = CF::Array.immutable(cf_strings) + + expect { array.to_ruby }.not_to raise_error + expect(array.to_ruby).to eq(file_paths) + end + + it 'should handle CFArrayApplyFunction callback correctly' do + cf_strings = file_paths.map { |path| CF::String.from_string(path) } + array = CF::Array.immutable(cf_strings) + + # This directly tests the each method which calls CFArrayApplyFunction + # where the segfault occurs at 0x0000000000000000 + values_from_callback = [] + expect { + range = CF::Range.new + range[:location] = 0 + range[:length] = array.length + callback = lambda do |value, _| + # value should be a valid pointer, not null + expect(value).not_to be_nil + expect(value.null?).to be false + values_from_callback << CF::Base.typecast(value) + end + CF.CFArrayApplyFunction(array, range, callback, nil) + }.not_to raise_error + + expect(values_from_callback.length).to eq(2) + end + end + + context 'with a mutable array of file paths' do + let(:file_paths) do + [ + "/Library/Managed Installs/fake.log", + "/Library/Managed Installs/also_fake.log" + ] + end + + it 'should successfully append and iterate over file paths in mutable array' do + array = CF::Array.mutable + + file_paths.each do |path| + cf_string = CF::String.from_string(path) + expect { array << cf_string }.not_to raise_error + end + + expect(array.length).to eq(2) + + collected_values = [] + expect { + array.each do |value| + collected_values << value.to_ruby + end + }.not_to raise_error + + expect(collected_values).to eq(file_paths) + end + end + + context 'with refinements (simulating Chef usage pattern)' do + using CF::Refinements + + let(:file_paths) do + [ + "/Library/Managed Installs/fake.log", + "/Library/Managed Installs/also_fake.log" + ] + end + + it 'should convert Ruby array to CF array using refinements without segfault' do + cf_array = nil + expect { + cf_array = file_paths.to_cf + }.not_to raise_error + + expect(cf_array).to be_a(CF::Array) + expect(cf_array.length).to eq(2) + end + + it 'should iterate over CF array created from refinements without segfault' do + cf_array = file_paths.to_cf + + collected_values = [] + expect { + cf_array.each do |value| + collected_values << value + end + }.not_to raise_error + + expect(collected_values.length).to eq(2) + expect(collected_values.map(&:to_ruby)).to eq(file_paths) + end + + it 'should successfully use array in preferences-like scenario' do + # This simulates the usage pattern in Chef's macos_userdefaults + cf_array = file_paths.to_cf + + # Test that we can retrieve individual elements + expect { cf_array[0] }.not_to raise_error + expect { cf_array[1] }.not_to raise_error + + expect(cf_array[0].to_ruby).to eq(file_paths[0]) + expect(cf_array[1].to_ruby).to eq(file_paths[1]) + + # Test that we can convert back to Ruby + expect { cf_array.to_ruby }.not_to raise_error + expect(cf_array.to_ruby).to eq(file_paths) + end + end + end end \ No newline at end of file