diff --git a/tests/Unit/API/Baggage/BaggageBuilderTest.php b/tests/Unit/API/Baggage/BaggageBuilderTest.php new file mode 100644 index 000000000..19c2b4033 --- /dev/null +++ b/tests/Unit/API/Baggage/BaggageBuilderTest.php @@ -0,0 +1,71 @@ +assertInstanceOf(BaggageInterface::class, $builder->build()); + } + + public function test_set_adds_entry(): void + { + $builder = new BaggageBuilder(); + $result = $builder->set('key', 'value'); + $this->assertSame($builder, $result); + $baggage = $builder->build(); + $entry = $baggage->getEntry('key'); + $this->assertNotNull($entry); + $this->assertSame('value', $entry->getValue()); + } + + public function test_set_with_metadata(): void + { + $metadata = new Metadata('meta'); + $builder = new BaggageBuilder(); + $builder->set('key', 'value', $metadata); + $baggage = $builder->build(); + $entry = $baggage->getEntry('key'); + $this->assertNotNull($entry); + $this->assertSame('meta', $entry->getMetadata()->getValue()); + } + + public function test_set_empty_key_is_ignored(): void + { + $builder = new BaggageBuilder(); + $result = $builder->set('', 'value'); + $this->assertSame($builder, $result); + $baggage = $builder->build(); + $this->assertNull($baggage->getEntry('')); + } + + public function test_remove_removes_entry(): void + { + $builder = new BaggageBuilder(); + $builder->set('key', 'value'); + $result = $builder->remove('key'); + $this->assertSame($builder, $result); + $baggage = $builder->build(); + $this->assertNull($baggage->getEntry('key')); + } + + public function test_set_overwrites_existing(): void + { + $builder = new BaggageBuilder(); + $builder->set('key', 'first'); + $builder->set('key', 'second'); + $baggage = $builder->build(); + $this->assertSame('second', $baggage->getEntry('key')->getValue()); + } +} diff --git a/tests/Unit/API/Baggage/MetadataTest.php b/tests/Unit/API/Baggage/MetadataTest.php new file mode 100644 index 000000000..05d56ebb5 --- /dev/null +++ b/tests/Unit/API/Baggage/MetadataTest.php @@ -0,0 +1,47 @@ +assertSame('some-metadata', $metadata->getValue()); + } + + public function test_get_empty_returns_metadata_with_empty_value(): void + { + $metadata = Metadata::getEmpty(); + + $this->assertSame('', $metadata->getValue()); + } + + public function test_get_empty_returns_same_instance(): void + { + $this->assertSame(Metadata::getEmpty(), Metadata::getEmpty()); + } + + public function test_constructor_with_empty_string(): void + { + $metadata = new Metadata(''); + + $this->assertSame('', $metadata->getValue()); + } + + public function test_constructor_with_complex_value(): void + { + $value = 'key1=value1;key2=value2'; + $metadata = new Metadata($value); + + $this->assertSame($value, $metadata->getValue()); + } +} diff --git a/tests/Unit/API/Behavior/Internal/LogWriter/ErrorLogWriterTest.php b/tests/Unit/API/Behavior/Internal/LogWriter/ErrorLogWriterTest.php new file mode 100644 index 000000000..c0e165e0f --- /dev/null +++ b/tests/Unit/API/Behavior/Internal/LogWriter/ErrorLogWriterTest.php @@ -0,0 +1,54 @@ +assertNotFalse($file); + + try { + ini_set('error_log', $file); + $writer->write('warning', 'test error log message', []); + + $contents = file_get_contents($file); + $this->assertStringContainsString('test error log message', $contents); + } finally { + ini_restore('error_log'); + unlink($file); + } + } + + public function test_write_with_exception_context(): void + { + $writer = new ErrorLogWriter(); + + $file = tempnam(sys_get_temp_dir(), 'otel_errorlog_'); + $this->assertNotFalse($file); + + try { + ini_set('error_log', $file); + $exception = new \RuntimeException('boom'); + $writer->write('error', 'failure occurred', ['exception' => $exception]); + + $contents = file_get_contents($file); + $this->assertStringContainsString('failure occurred', $contents); + $this->assertStringContainsString('boom', $contents); + } finally { + ini_restore('error_log'); + unlink($file); + } + } +} diff --git a/tests/Unit/API/Behavior/Internal/LogWriter/NoopLogWriterTest.php b/tests/Unit/API/Behavior/Internal/LogWriter/NoopLogWriterTest.php new file mode 100644 index 000000000..309d848fb --- /dev/null +++ b/tests/Unit/API/Behavior/Internal/LogWriter/NoopLogWriterTest.php @@ -0,0 +1,24 @@ +write('info', 'this message is discarded', []); + $writer->write('error', 'this too', ['exception' => new \RuntimeException('ignored')]); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/API/Behavior/Internal/LogWriter/Psr3LogWriterTest.php b/tests/Unit/API/Behavior/Internal/LogWriter/Psr3LogWriterTest.php new file mode 100644 index 000000000..388a0ad3d --- /dev/null +++ b/tests/Unit/API/Behavior/Internal/LogWriter/Psr3LogWriterTest.php @@ -0,0 +1,50 @@ +createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('log') + ->with('warning', 'test message', ['key' => 'value']); + + $writer = new Psr3LogWriter($logger); + $writer->write('warning', 'test message', ['key' => 'value']); + } + + public function test_write_passes_level_through(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('log') + ->with('error', 'error message', []); + + $writer = new Psr3LogWriter($logger); + $writer->write('error', 'error message', []); + } + + public function test_write_passes_context_with_exception(): void + { + $exception = new \RuntimeException('test exception'); + $context = ['exception' => $exception, 'extra' => 'data']; + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('log') + ->with('critical', 'something broke', $context); + + $writer = new Psr3LogWriter($logger); + $writer->write('critical', 'something broke', $context); + } +} diff --git a/tests/Unit/API/Behavior/Internal/LogWriter/StreamLogWriterTest.php b/tests/Unit/API/Behavior/Internal/LogWriter/StreamLogWriterTest.php new file mode 100644 index 000000000..9fef575e2 --- /dev/null +++ b/tests/Unit/API/Behavior/Internal/LogWriter/StreamLogWriterTest.php @@ -0,0 +1,53 @@ +assertNotFalse($file); + + try { + $writer = new StreamLogWriter($file); + $writer->write('warning', 'test message', []); + + $contents = file_get_contents($file); + $this->assertStringContainsString('test message', $contents); + } finally { + unlink($file); + } + } + + public function test_constructor_throws_for_invalid_destination(): void + { + $this->expectException(\RuntimeException::class); + new StreamLogWriter('/nonexistent/path/that/should/not/exist/file.log'); + } + + public function test_write_with_exception_context(): void + { + $file = tempnam(sys_get_temp_dir(), 'otel_test_'); + $this->assertNotFalse($file); + + try { + $writer = new StreamLogWriter($file); + $exception = new \RuntimeException('test exception'); + $writer->write('error', 'something failed', ['exception' => $exception]); + + $contents = file_get_contents($file); + $this->assertStringContainsString('something failed', $contents); + $this->assertStringContainsString('test exception', $contents); + } finally { + unlink($file); + } + } +} diff --git a/tests/Unit/API/Behavior/Internal/LogWriterFactoryTest.php b/tests/Unit/API/Behavior/Internal/LogWriterFactoryTest.php index 41eac85e1..f658e65a8 100644 --- a/tests/Unit/API/Behavior/Internal/LogWriterFactoryTest.php +++ b/tests/Unit/API/Behavior/Internal/LogWriterFactoryTest.php @@ -53,4 +53,24 @@ public function test_psr3_log_destination(): void LoggerHolder::set($this->createMock(LoggerInterface::class)); $this->assertInstanceOf(Psr3LogWriter::class, (new LogWriterFactory())->create()); } + + public function test_psr3_from_env_with_logger(): void + { + $this->setEnvironmentVariable('OTEL_PHP_LOG_DESTINATION', 'psr3'); + LoggerHolder::set($this->createMock(LoggerInterface::class)); + $this->assertInstanceOf(Psr3LogWriter::class, (new LogWriterFactory())->create()); + } + + public function test_psr3_from_env_without_logger_falls_back_to_error_log(): void + { + $this->setEnvironmentVariable('OTEL_PHP_LOG_DESTINATION', 'psr3'); + $this->assertInstanceOf(ErrorLogWriter::class, (new LogWriterFactory())->create()); + } + + public function test_default_with_logger_uses_psr3(): void + { + LoggerHolder::set($this->createMock(LoggerInterface::class)); + $this->setEnvironmentVariable('OTEL_PHP_LOG_DESTINATION', ''); + $this->assertInstanceOf(Psr3LogWriter::class, (new LogWriterFactory())->create()); + } } diff --git a/tests/Unit/API/Behavior/Internal/LoggingTest.php b/tests/Unit/API/Behavior/Internal/LoggingTest.php new file mode 100644 index 000000000..449d75630 --- /dev/null +++ b/tests/Unit/API/Behavior/Internal/LoggingTest.php @@ -0,0 +1,74 @@ +createMock(LogWriterInterface::class); + Logging::setLogWriter($writer); + $this->assertSame($writer, Logging::logWriter()); + } + + public function test_log_writer_creates_default(): void + { + $writer = Logging::logWriter(); + $this->assertInstanceOf(LogWriterInterface::class, $writer); + } + + public function test_disable_sets_noop_writer(): void + { + Logging::disable(); + $this->assertInstanceOf(NoopLogWriter::class, Logging::logWriter()); + } + + public function test_reset_clears_state(): void + { + $writer = $this->createMock(LogWriterInterface::class); + Logging::setLogWriter($writer); + Logging::reset(); + $this->assertNotSame($writer, Logging::logWriter()); + } + + public function test_level_returns_index(): void + { + // 'debug' is index 0 but level() returns ?: 1, so debug maps to info level + $this->assertSame(1, Logging::level('debug')); + $this->assertSame(1, Logging::level('info')); + $this->assertSame(3, Logging::level('warning')); + $this->assertSame(4, Logging::level('error')); + } + + public function test_level_returns_info_for_unknown(): void + { + $this->assertSame(1, Logging::level('unknown')); + } + + public function test_log_level_returns_default(): void + { + $level = Logging::logLevel(); + $this->assertIsInt($level); + } +} diff --git a/tests/Unit/API/Common/Time/TestClockTest.php b/tests/Unit/API/Common/Time/TestClockTest.php new file mode 100644 index 000000000..f4c6d3afc --- /dev/null +++ b/tests/Unit/API/Common/Time/TestClockTest.php @@ -0,0 +1,61 @@ +assertSame(TestClock::DEFAULT_START_EPOCH, $clock->now()); + } + + public function test_custom_start_epoch(): void + { + $clock = new TestClock(42); + $this->assertSame(42, $clock->now()); + } + + public function test_advance(): void + { + $clock = new TestClock(100); + $clock->advance(50); + $this->assertSame(150, $clock->now()); + } + + public function test_advance_default_one_nano(): void + { + $clock = new TestClock(100); + $clock->advance(); + $this->assertSame(101, $clock->now()); + } + + public function test_advance_seconds(): void + { + $clock = new TestClock(0); + $clock->advanceSeconds(2); + $this->assertSame(2 * ClockInterface::NANOS_PER_SECOND, $clock->now()); + } + + public function test_advance_seconds_default_one(): void + { + $clock = new TestClock(0); + $clock->advanceSeconds(); + $this->assertSame(ClockInterface::NANOS_PER_SECOND, $clock->now()); + } + + public function test_set_time(): void + { + $clock = new TestClock(100); + $clock->setTime(999); + $this->assertSame(999, $clock->now()); + } +} diff --git a/tests/Unit/API/Logs/NoopLogRecordBuilderTest.php b/tests/Unit/API/Logs/NoopLogRecordBuilderTest.php new file mode 100644 index 000000000..90fd392fd --- /dev/null +++ b/tests/Unit/API/Logs/NoopLogRecordBuilderTest.php @@ -0,0 +1,78 @@ +builder = new NoopLogRecordBuilder(); + } + + public function test_set_timestamp_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setTimestamp(123)); + } + + public function test_set_observed_timestamp_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setObservedTimestamp(123)); + } + + public function test_set_context_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setContext(null)); + } + + public function test_set_severity_number_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setSeverityNumber(Severity::INFO)); + } + + public function test_set_severity_text_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setSeverityText('INFO')); + } + + public function test_set_body_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setBody('test')); + } + + public function test_set_attribute_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setAttribute('key', 'value')); + } + + public function test_set_attributes_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setAttributes([])); + } + + public function test_set_exception_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setException(new \Exception('test'))); + } + + public function test_set_event_name_returns_self(): void + { + $this->assertSame($this->builder, $this->builder->setEventName('event')); + } + + public function test_emit_does_nothing(): void + { + $this->builder->emit(); + $this->assertTrue(true); // just verifying no exception + } +} diff --git a/tests/Unit/API/Metrics/LateBindingMeterTest.php b/tests/Unit/API/Metrics/LateBindingMeterTest.php new file mode 100644 index 000000000..88521296e --- /dev/null +++ b/tests/Unit/API/Metrics/LateBindingMeterTest.php @@ -0,0 +1,121 @@ +createMock(CounterInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createCounter')->with('test', 'unit', 'desc', [])->willReturn($counter); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($counter, $lateBinding->createCounter('test', 'unit', 'desc')); + } + + public function test_create_histogram_delegates_to_meter(): void + { + $histogram = $this->createMock(HistogramInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createHistogram')->willReturn($histogram); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($histogram, $lateBinding->createHistogram('test')); + } + + public function test_create_gauge_delegates_to_meter(): void + { + $gauge = $this->createMock(GaugeInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createGauge')->willReturn($gauge); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($gauge, $lateBinding->createGauge('test')); + } + + public function test_create_up_down_counter_delegates_to_meter(): void + { + $counter = $this->createMock(UpDownCounterInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createUpDownCounter')->willReturn($counter); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($counter, $lateBinding->createUpDownCounter('test')); + } + + public function test_create_observable_counter_delegates_to_meter(): void + { + $counter = $this->createMock(ObservableCounterInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createObservableCounter')->willReturn($counter); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($counter, $lateBinding->createObservableCounter('test')); + } + + public function test_create_observable_gauge_delegates_to_meter(): void + { + $gauge = $this->createMock(ObservableGaugeInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createObservableGauge')->willReturn($gauge); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($gauge, $lateBinding->createObservableGauge('test')); + } + + public function test_create_observable_up_down_counter_delegates_to_meter(): void + { + $counter = $this->createMock(ObservableUpDownCounterInterface::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('createObservableUpDownCounter')->willReturn($counter); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($counter, $lateBinding->createObservableUpDownCounter('test')); + } + + public function test_batch_observe_delegates_to_meter(): void + { + $callback = $this->createMock(ObservableCallbackInterface::class); + $instrument = $this->createMock(AsynchronousInstrument::class); + $meter = $this->createMock(MeterInterface::class); + $meter->expects($this->once())->method('batchObserve')->willReturn($callback); + + $lateBinding = new LateBindingMeter(fn () => $meter); + $this->assertSame($callback, $lateBinding->batchObserve(fn () => null, $instrument)); + } + + public function test_factory_called_only_once(): void + { + $callCount = 0; + $meter = $this->createMock(MeterInterface::class); + $meter->method('createCounter')->willReturn($this->createMock(CounterInterface::class)); + $meter->method('createHistogram')->willReturn($this->createMock(HistogramInterface::class)); + + $lateBinding = new LateBindingMeter(function () use ($meter, &$callCount) { + $callCount++; + return $meter; + }); + + $lateBinding->createCounter('a'); + $lateBinding->createHistogram('b'); + $this->assertSame(1, $callCount); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopCounterTest.php b/tests/Unit/API/Metrics/Noop/NoopCounterTest.php new file mode 100644 index 000000000..0401e9e7d --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopCounterTest.php @@ -0,0 +1,33 @@ +counter = new NoopCounter(); + } + + public function test_add_does_not_throw(): void + { + $this->counter->add(1); + $this->counter->add(5, ['key' => 'value']); + $this->expectNotToPerformAssertions(); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->counter->isEnabled()); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopGaugeTest.php b/tests/Unit/API/Metrics/Noop/NoopGaugeTest.php new file mode 100644 index 000000000..dfd07d65c --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopGaugeTest.php @@ -0,0 +1,33 @@ +gauge = new NoopGauge(); + } + + public function test_record_does_not_throw(): void + { + $this->gauge->record(1); + $this->gauge->record(3.14, ['key' => 'value']); + $this->expectNotToPerformAssertions(); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->gauge->isEnabled()); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopHistogramTest.php b/tests/Unit/API/Metrics/Noop/NoopHistogramTest.php new file mode 100644 index 000000000..32f88b36f --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopHistogramTest.php @@ -0,0 +1,33 @@ +histogram = new NoopHistogram(); + } + + public function test_record_does_not_throw(): void + { + $this->histogram->record(1); + $this->histogram->record(3.14, ['key' => 'value']); + $this->expectNotToPerformAssertions(); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->histogram->isEnabled()); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopMeterTest.php b/tests/Unit/API/Metrics/Noop/NoopMeterTest.php new file mode 100644 index 000000000..b773af7b2 --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopMeterTest.php @@ -0,0 +1,71 @@ +meter = new NoopMeter(); + } + + public function test_create_counter(): void + { + $this->assertInstanceOf(CounterInterface::class, $this->meter->createCounter('test')); + } + + public function test_create_histogram(): void + { + $this->assertInstanceOf(HistogramInterface::class, $this->meter->createHistogram('test')); + } + + public function test_create_gauge(): void + { + $this->assertInstanceOf(GaugeInterface::class, $this->meter->createGauge('test')); + } + + public function test_create_up_down_counter(): void + { + $this->assertInstanceOf(UpDownCounterInterface::class, $this->meter->createUpDownCounter('test')); + } + + public function test_create_observable_counter(): void + { + $this->assertInstanceOf(ObservableCounterInterface::class, $this->meter->createObservableCounter('test')); + } + + public function test_create_observable_gauge(): void + { + $this->assertInstanceOf(ObservableGaugeInterface::class, $this->meter->createObservableGauge('test')); + } + + public function test_create_observable_up_down_counter(): void + { + $this->assertInstanceOf(ObservableUpDownCounterInterface::class, $this->meter->createObservableUpDownCounter('test')); + } + + public function test_batch_observe(): void + { + $instrument = $this->createMock(AsynchronousInstrument::class); + $this->assertInstanceOf(ObservableCallbackInterface::class, $this->meter->batchObserve(fn () => null, $instrument)); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopObservableCallbackTest.php b/tests/Unit/API/Metrics/Noop/NoopObservableCallbackTest.php new file mode 100644 index 000000000..a71d4375e --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopObservableCallbackTest.php @@ -0,0 +1,20 @@ +detach(); + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopObservableCounterTest.php b/tests/Unit/API/Metrics/Noop/NoopObservableCounterTest.php new file mode 100644 index 000000000..a008b86dd --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopObservableCounterTest.php @@ -0,0 +1,35 @@ +counter = new NoopObservableCounter(); + } + + public function test_observe_returns_noop_observable_callback(): void + { + $callback = $this->counter->observe(fn () => null); + $this->assertInstanceOf(ObservableCallbackInterface::class, $callback); + $this->assertInstanceOf(NoopObservableCallback::class, $callback); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->counter->isEnabled()); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopObservableGaugeTest.php b/tests/Unit/API/Metrics/Noop/NoopObservableGaugeTest.php new file mode 100644 index 000000000..3ee2493dd --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopObservableGaugeTest.php @@ -0,0 +1,35 @@ +gauge = new NoopObservableGauge(); + } + + public function test_observe_returns_noop_observable_callback(): void + { + $callback = $this->gauge->observe(fn () => null); + $this->assertInstanceOf(ObservableCallbackInterface::class, $callback); + $this->assertInstanceOf(NoopObservableCallback::class, $callback); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->gauge->isEnabled()); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopObservableUpDownCounterTest.php b/tests/Unit/API/Metrics/Noop/NoopObservableUpDownCounterTest.php new file mode 100644 index 000000000..c1abbc9ed --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopObservableUpDownCounterTest.php @@ -0,0 +1,35 @@ +counter = new NoopObservableUpDownCounter(); + } + + public function test_observe_returns_noop_observable_callback(): void + { + $callback = $this->counter->observe(fn () => null); + $this->assertInstanceOf(ObservableCallbackInterface::class, $callback); + $this->assertInstanceOf(NoopObservableCallback::class, $callback); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->counter->isEnabled()); + } +} diff --git a/tests/Unit/API/Metrics/Noop/NoopUpDownCounterTest.php b/tests/Unit/API/Metrics/Noop/NoopUpDownCounterTest.php new file mode 100644 index 000000000..c7b0b894b --- /dev/null +++ b/tests/Unit/API/Metrics/Noop/NoopUpDownCounterTest.php @@ -0,0 +1,33 @@ +counter = new NoopUpDownCounter(); + } + + public function test_add_does_not_throw(): void + { + $this->counter->add(1); + $this->counter->add(-5, ['key' => 'value']); + $this->expectNotToPerformAssertions(); + } + + public function test_is_enabled_returns_false(): void + { + $this->assertFalse($this->counter->isEnabled()); + } +} diff --git a/tests/Unit/API/Trace/NoopTracerProviderTest.php b/tests/Unit/API/Trace/NoopTracerProviderTest.php new file mode 100644 index 000000000..45482d7b4 --- /dev/null +++ b/tests/Unit/API/Trace/NoopTracerProviderTest.php @@ -0,0 +1,30 @@ +getTracer('test'); + $this->assertInstanceOf(TracerInterface::class, $tracer); + $this->assertInstanceOf(NoopTracer::class, $tracer); + } + + public function test_get_tracer_with_all_parameters(): void + { + $provider = new NoopTracerProvider(); + $tracer = $provider->getTracer('test', '1.0.0', 'https://example.com', []); + $this->assertInstanceOf(NoopTracer::class, $tracer); + } +} diff --git a/tests/Unit/API/Trace/SpanTest.php b/tests/Unit/API/Trace/SpanTest.php new file mode 100644 index 000000000..e972f3eda --- /dev/null +++ b/tests/Unit/API/Trace/SpanTest.php @@ -0,0 +1,63 @@ +assertInstanceOf(NonRecordingSpan::class, $span); + $this->assertFalse($span->getContext()->isValid()); + } + + public function test_get_invalid_returns_same_instance(): void + { + $this->assertSame(Span::getInvalid(), Span::getInvalid()); + } + + public function test_wrap_valid_context_returns_non_recording_span(): void + { + $context = SpanContext::create('0af7651916cd43dd8448eb211c80319c', 'b7ad6b7169203331'); + $span = Span::wrap($context); + $this->assertInstanceOf(NonRecordingSpan::class, $span); + $this->assertTrue($span->getContext()->isValid()); + } + + public function test_wrap_invalid_context_returns_invalid_span(): void + { + $span = Span::wrap(SpanContext::getInvalid()); + $this->assertSame(Span::getInvalid(), $span); + } + + public function test_from_context_returns_span(): void + { + $spanContext = SpanContext::create('0af7651916cd43dd8448eb211c80319c', 'b7ad6b7169203331'); + $span = Span::wrap($spanContext); + $context = $span->storeInContext(Context::getRoot()); + $this->assertSame($span, Span::fromContext($context)); + } + + public function test_from_context_returns_invalid_when_no_span(): void + { + $span = Span::fromContext(Context::getRoot()); + $this->assertSame(Span::getInvalid(), $span); + } + + public function test_get_current_returns_span(): void + { + $span = Span::getCurrent(); + $this->assertInstanceOf(SpanInterface::class, $span); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Detector/ComposerTest.php b/tests/Unit/Config/SDK/ComponentProvider/Detector/ComposerTest.php new file mode 100644 index 000000000..b34bd77c7 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Detector/ComposerTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new Composer(); + $detector = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(ComposerDetector::class, $detector); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Detector/HostTest.php b/tests/Unit/Config/SDK/ComponentProvider/Detector/HostTest.php new file mode 100644 index 000000000..2ca80ae0a --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Detector/HostTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new Host(); + $detector = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(HostDetector::class, $detector); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Detector/ProcessTest.php b/tests/Unit/Config/SDK/ComponentProvider/Detector/ProcessTest.php new file mode 100644 index 000000000..898060aac --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Detector/ProcessTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new Process(); + $detector = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(ProcessDetector::class, $detector); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Detector/ServiceTest.php b/tests/Unit/Config/SDK/ComponentProvider/Detector/ServiceTest.php new file mode 100644 index 000000000..6c9ba3444 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Detector/ServiceTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new Service(); + $detector = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(ServiceDetector::class, $detector); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Instrumentation/General/HttpConfigProviderTest.php b/tests/Unit/Config/SDK/ComponentProvider/Instrumentation/General/HttpConfigProviderTest.php new file mode 100644 index 000000000..2d270f801 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Instrumentation/General/HttpConfigProviderTest.php @@ -0,0 +1,55 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new HttpConfigProvider(); + $properties = [ + 'client' => [ + 'request_captured_headers' => ['Content-Type'], + 'response_captured_headers' => ['X-Request-Id'], + ], + 'server' => [ + 'request_captured_headers' => ['Accept'], + 'response_captured_headers' => ['Content-Length'], + ], + ]; + + $result = $provider->createPlugin($properties, new Context()); + $this->assertInstanceOf(GeneralInstrumentationConfiguration::class, $result); + $this->assertInstanceOf(HttpConfig::class, $result); + } + + public function test_create_plugin_empty_properties(): void + { + $provider = new HttpConfigProvider(); + $properties = []; + + $result = $provider->createPlugin($properties, new Context()); + $this->assertInstanceOf(HttpConfig::class, $result); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Instrumentation/General/PeerConfigProviderTest.php b/tests/Unit/Config/SDK/ComponentProvider/Instrumentation/General/PeerConfigProviderTest.php new file mode 100644 index 000000000..621910ce0 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Instrumentation/General/PeerConfigProviderTest.php @@ -0,0 +1,51 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new PeerConfigProvider(); + $properties = [ + 'service_mapping' => [ + ['peer' => '10.0.0.1', 'service' => 'my-service'], + ['peer' => '10.0.0.2', 'service' => 'other-service'], + ], + ]; + + $result = $provider->createPlugin($properties, new Context()); + $this->assertInstanceOf(GeneralInstrumentationConfiguration::class, $result); + $this->assertInstanceOf(PeerConfig::class, $result); + } + + public function test_create_plugin_empty_properties(): void + { + $provider = new PeerConfigProvider(); + $properties = []; + + $result = $provider->createPlugin($properties, new Context()); + $this->assertInstanceOf(PeerConfig::class, $result); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/InstrumentationConfigurationRegistryTest.php b/tests/Unit/Config/SDK/ComponentProvider/InstrumentationConfigurationRegistryTest.php new file mode 100644 index 000000000..50530d8d7 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/InstrumentationConfigurationRegistryTest.php @@ -0,0 +1,92 @@ +createMock(ComponentProviderRegistry::class); + $registry->method('componentMap')->willReturn(new ArrayNodeDefinition('test')); + + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin_empty_properties(): void + { + $provider = new InstrumentationConfigurationRegistry(); + + $result = $provider->createPlugin([ + 'instrumentation/development' => [ + 'php' => [], + 'general' => [], + ], + ], new Context()); + + $this->assertInstanceOf(ConfigurationRegistry::class, $result); + } + + public function test_create_plugin_with_php_configurations(): void + { + $mockConfig = $this->createMock(InstrumentationConfiguration::class); + $phpPlugin = $this->createMock(ComponentPlugin::class); + $phpPlugin->method('create')->willReturn($mockConfig); + + $provider = new InstrumentationConfigurationRegistry(); + + $result = $provider->createPlugin([ + 'instrumentation/development' => [ + 'php' => [$phpPlugin], + 'general' => [], + ], + ], new Context()); + + $this->assertInstanceOf(ConfigurationRegistry::class, $result); + } + + public function test_create_plugin_with_general_configurations(): void + { + $mockConfig = $this->createMock(GeneralInstrumentationConfiguration::class); + $generalPlugin = $this->createMock(ComponentPlugin::class); + $generalPlugin->method('create')->willReturn($mockConfig); + + $provider = new InstrumentationConfigurationRegistry(); + + $result = $provider->createPlugin([ + 'instrumentation/development' => [ + 'php' => [], + 'general' => [$generalPlugin], + ], + ], new Context()); + + $this->assertInstanceOf(ConfigurationRegistry::class, $result); + } + + public function test_create_plugin_with_null_sections(): void + { + $provider = new InstrumentationConfigurationRegistry(); + + $result = $provider->createPlugin([ + 'instrumentation/development' => [], + ], new Context()); + + $this->assertInstanceOf(ConfigurationRegistry::class, $result); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterConsoleTest.php b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterConsoleTest.php new file mode 100644 index 000000000..adde5a0dc --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterConsoleTest.php @@ -0,0 +1,36 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new LogRecordExporterConsole(); + $exporter = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(ConsoleExporter::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterMemoryTest.php b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterMemoryTest.php new file mode 100644 index 000000000..a3d1d26bc --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterMemoryTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new LogRecordExporterMemory(); + $exporter = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(InMemoryExporter::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterOtlpFileTest.php b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterOtlpFileTest.php new file mode 100644 index 000000000..d3b4f66c1 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordExporterOtlpFileTest.php @@ -0,0 +1,36 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new LogRecordExporterOtlpFile(); + $exporter = $provider->createPlugin(['output_stream' => 'stdout'], new Context()); + $this->assertInstanceOf(LogRecordExporterInterface::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordProcessorSimpleTest.php b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordProcessorSimpleTest.php new file mode 100644 index 000000000..fb525fc48 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Logs/LogRecordProcessorSimpleTest.php @@ -0,0 +1,31 @@ +createMock(ComponentProviderRegistry::class); + $nodeDefinition = $this->createMock(NodeDefinition::class); + $nodeDefinition->method('isRequired')->willReturnSelf(); + $registry->method('component') + ->with('exporter', LogRecordExporterInterface::class) + ->willReturn($nodeDefinition); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Metrics/AggregationResolverDefaultTest.php b/tests/Unit/Config/SDK/ComponentProvider/Metrics/AggregationResolverDefaultTest.php new file mode 100644 index 000000000..229f917e3 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Metrics/AggregationResolverDefaultTest.php @@ -0,0 +1,24 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterConsoleTest.php b/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterConsoleTest.php new file mode 100644 index 000000000..82caf2c85 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterConsoleTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new MetricExporterConsole(); + $exporter = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(ConsoleMetricExporter::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterMemoryTest.php b/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterMemoryTest.php new file mode 100644 index 000000000..7035f0391 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterMemoryTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new MetricExporterMemory(); + $exporter = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(InMemoryExporter::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterOtlpFileTest.php b/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterOtlpFileTest.php new file mode 100644 index 000000000..f7c9a1ede --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Metrics/MetricExporterOtlpFileTest.php @@ -0,0 +1,64 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin_cumulative(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new MetricExporterOtlpFile(); + $exporter = $provider->createPlugin([ + 'output_stream' => 'stdout', + 'temporality_preference' => 'cumulative', + 'default_histogram_aggregation' => 'explicit_bucket_histogram', + ], new Context()); + $this->assertInstanceOf(MetricExporterInterface::class, $exporter); + } + + public function test_create_plugin_delta(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new MetricExporterOtlpFile(); + $exporter = $provider->createPlugin([ + 'output_stream' => 'stdout', + 'temporality_preference' => 'delta', + 'default_histogram_aggregation' => 'explicit_bucket_histogram', + ], new Context()); + $this->assertInstanceOf(MetricExporterInterface::class, $exporter); + } + + public function test_create_plugin_lowmemory(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new MetricExporterOtlpFile(); + $exporter = $provider->createPlugin([ + 'output_stream' => 'stdout', + 'temporality_preference' => 'lowmemory', + 'default_histogram_aggregation' => 'explicit_bucket_histogram', + ], new Context()); + $this->assertInstanceOf(MetricExporterInterface::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/OpenTelemetrySdkTest.php b/tests/Unit/Config/SDK/ComponentProvider/OpenTelemetrySdkTest.php new file mode 100644 index 000000000..efa61ca92 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/OpenTelemetrySdkTest.php @@ -0,0 +1,429 @@ +createMock(ComponentProviderRegistry::class); + $registry->method('component')->willReturn(new ArrayNodeDefinition('test')); + $registry->method('componentList')->willReturn(new ArrayNodeDefinition('test')); + + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin_disabled(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties(disabled: true); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_no_propagators(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties(disabled: true); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_propagators(): void + { + $mockPropagator = $this->createMock(TextMapPropagatorInterface::class); + $propagatorPlugin = $this->createMock(ComponentPlugin::class); + $propagatorPlugin->method('create')->willReturn($mockPropagator); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: true, + propagatorPlugins: [$propagatorPlugin], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_response_propagators(): void + { + $mockPropagator = $this->createMock(ResponsePropagatorInterface::class); + $propagatorPlugin = $this->createMock(ComponentPlugin::class); + $propagatorPlugin->method('create')->willReturn($mockPropagator); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: true, + responsePropagatorPlugins: [$propagatorPlugin], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_enabled_minimal(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties(disabled: false); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_span_processors(): void + { + $mockProcessor = $this->createMock(SpanProcessorInterface::class); + $mockProcessor->method('forceFlush')->willReturn(true); + $mockProcessor->method('shutdown')->willReturn(true); + $processorPlugin = $this->createMock(ComponentPlugin::class); + $processorPlugin->method('create')->willReturn($mockProcessor); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + spanProcessorPlugins: [$processorPlugin], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_sampler(): void + { + $mockSampler = $this->createMock(SamplerInterface::class); + $samplerPlugin = $this->createMock(ComponentPlugin::class); + $samplerPlugin->method('create')->willReturn($mockSampler); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + samplerPlugin: $samplerPlugin, + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_tracer_configurator(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + tracerConfigurator: [ + 'default_config' => ['disabled' => true], + 'tracers' => [ + ['name' => 'my-tracer', 'config' => ['disabled' => false]], + ], + ], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_meter_views(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + views: [ + [ + 'stream' => [ + 'name' => 'my-view', + 'description' => 'desc', + 'aggregation_cardinality_limit' => null, + 'attribute_keys' => ['included' => ['key1'], 'excluded' => []], + 'aggregation' => null, + ], + 'selector' => [ + 'instrument_type' => 'counter', + 'instrument_name' => 'my.instrument', + 'unit' => 'ms', + 'meter_name' => 'my-meter', + 'meter_version' => '1.0.0', + 'meter_schema_url' => 'https://example.com', + ], + ], + ], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_all_instrument_types(): void + { + $types = ['counter', 'histogram', 'observable_counter', 'observable_gauge', 'observable_up_down_counter', 'up_down_counter']; + + foreach ($types as $type) { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + views: [ + [ + 'stream' => [ + 'name' => null, + 'description' => null, + 'aggregation_cardinality_limit' => null, + 'attribute_keys' => ['included' => null, 'excluded' => []], + ], + 'selector' => [ + 'instrument_type' => $type, + 'instrument_name' => null, + 'unit' => null, + 'meter_name' => null, + 'meter_version' => null, + 'meter_schema_url' => null, + ], + ], + ], + ); + $result = $provider->createPlugin($properties, new Context()); + $this->assertInstanceOf(SdkBuilder::class, $result, "Failed for instrument type: $type"); + } + } + + public function test_create_plugin_with_meter_configurator(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + meterConfigurator: [ + 'default_config' => ['disabled' => false], + 'meters' => [ + ['name' => 'my-meter', 'config' => ['disabled' => true]], + ], + ], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_log_processors(): void + { + $mockProcessor = $this->createMock(LogRecordProcessorInterface::class); + $processorPlugin = $this->createMock(ComponentPlugin::class); + $processorPlugin->method('create')->willReturn($mockProcessor); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + logProcessorPlugins: [$processorPlugin], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_logger_configurator(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + loggerConfigurator: [ + 'default_config' => ['disabled' => true], + 'loggers' => [ + ['name' => 'my-logger', 'config' => ['disabled' => false]], + ], + ], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_resource_detectors(): void + { + $mockDetector = $this->createMock(ResourceDetectorInterface::class); + $mockDetector->method('getResource')->willReturn( + \OpenTelemetry\SDK\Resource\ResourceInfo::create(\OpenTelemetry\SDK\Common\Attribute\Attributes::create([])) + ); + $detectorPlugin = $this->createMock(ComponentPlugin::class); + $detectorPlugin->method('create')->willReturn($mockDetector); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + resourceDetectorPlugins: [$detectorPlugin], + resourceIncluded: ['key1'], + resourceExcluded: ['key2'], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_resource_schema_url(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + schemaUrl: 'https://example.com/schema', + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_resource_attributes(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + resourceAttributes: [ + ['name' => 'service.name', 'value' => 'my-service', 'type' => 'string'], + ], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_attributes_list(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + attributesList: 'service.name=my-service,service.version=1.0', + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_metric_readers(): void + { + $mockReader = $this->createMock(MetricReaderInterface::class); + $readerPlugin = $this->createMock(ComponentPlugin::class); + $readerPlugin->method('create')->willReturn($mockReader); + + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + metricReaderPlugins: [$readerPlugin], + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + public function test_create_plugin_with_tracer_attribute_limits(): void + { + $provider = new OpenTelemetrySdk(); + $properties = $this->createProperties( + disabled: false, + tracerAttributeCountLimit: 64, + tracerAttributeValueLengthLimit: 256, + eventAttributeCountLimit: 32, + linkAttributeCountLimit: 16, + ); + $result = $provider->createPlugin($properties, new Context()); + + $this->assertInstanceOf(SdkBuilder::class, $result); + } + + private function createProperties( + bool $disabled = true, + array $propagatorPlugins = [], + array $responsePropagatorPlugins = [], + array $spanProcessorPlugins = [], + ?ComponentPlugin $samplerPlugin = null, + array $tracerConfigurator = null, + array $views = [], + array $metricReaderPlugins = [], + array $meterConfigurator = null, + array $logProcessorPlugins = [], + array $loggerConfigurator = null, + array $resourceDetectorPlugins = [], + ?array $resourceIncluded = null, + array $resourceExcluded = [], + ?string $schemaUrl = null, + array $resourceAttributes = [], + ?string $attributesList = null, + ?int $tracerAttributeCountLimit = null, + ?int $tracerAttributeValueLengthLimit = null, + ?int $eventAttributeCountLimit = null, + ?int $linkAttributeCountLimit = null, + ): array { + return [ + 'file_format' => '1.0-rc.2', + 'disabled' => $disabled, + 'resource' => [ + 'attributes' => $resourceAttributes, + 'attributes_list' => $attributesList, + 'detection/development' => [ + 'attributes' => [ + 'included' => $resourceIncluded, + 'excluded' => $resourceExcluded, + ], + 'detectors' => $resourceDetectorPlugins, + ], + 'schema_url' => $schemaUrl, + ], + 'attribute_limits' => [ + 'attribute_value_length_limit' => null, + 'attribute_count_limit' => 128, + ], + 'propagator' => [ + 'composite' => $propagatorPlugins, + ], + 'response_propagator/development' => [ + 'composite' => $responsePropagatorPlugins, + ], + 'tracer_provider' => [ + 'limits' => [ + 'attribute_value_length_limit' => $tracerAttributeValueLengthLimit, + 'attribute_count_limit' => $tracerAttributeCountLimit, + 'event_count_limit' => 128, + 'link_count_limit' => 128, + 'event_attribute_count_limit' => $eventAttributeCountLimit, + 'link_attribute_count_limit' => $linkAttributeCountLimit, + ], + 'sampler' => $samplerPlugin, + 'processors' => $spanProcessorPlugins, + 'tracer_configurator/development' => $tracerConfigurator, + ], + 'meter_provider' => [ + 'views' => $views, + 'readers' => $metricReaderPlugins, + 'exemplar_filter' => 'trace_based', + 'meter_configurator/development' => $meterConfigurator, + ], + 'logger_provider' => [ + 'limits' => [ + 'attribute_value_length_limit' => null, + 'attribute_count_limit' => null, + ], + 'processors' => $logProcessorPlugins, + 'logger_configurator/development' => $loggerConfigurator, + ], + ]; + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Propagator/ResponsePropagatorCompositeTest.php b/tests/Unit/Config/SDK/ComponentProvider/Propagator/ResponsePropagatorCompositeTest.php new file mode 100644 index 000000000..b768dcaff --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Propagator/ResponsePropagatorCompositeTest.php @@ -0,0 +1,64 @@ +createMock(ComponentProviderRegistry::class); + $registry->method('componentNames') + ->with('composite', ResponsePropagatorInterface::class) + ->willReturn(new ArrayNodeDefinition('composite')); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin_empty(): void + { + $provider = new ResponsePropagatorComposite(); + $result = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(MultiResponsePropagator::class, $result); + } + + public function test_create_plugin_with_propagators(): void + { + $mockPropagator = $this->createMock(ResponsePropagatorInterface::class); + $plugin = $this->createMock(ComponentPlugin::class); + $plugin->method('create')->willReturn($mockPropagator); + + $provider = new ResponsePropagatorComposite(); + $result = $provider->createPlugin([$plugin], new Context()); + $this->assertInstanceOf(MultiResponsePropagator::class, $result); + } + + public function test_create_plugin_with_multiple_propagators(): void + { + $mockPropagator1 = $this->createMock(ResponsePropagatorInterface::class); + $plugin1 = $this->createMock(ComponentPlugin::class); + $plugin1->method('create')->willReturn($mockPropagator1); + + $mockPropagator2 = $this->createMock(ResponsePropagatorInterface::class); + $plugin2 = $this->createMock(ComponentPlugin::class); + $plugin2->method('create')->willReturn($mockPropagator2); + + $provider = new ResponsePropagatorComposite(); + $result = $provider->createPlugin([$plugin1, $plugin2], new Context()); + $this->assertInstanceOf(MultiResponsePropagator::class, $result); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorBaggageTest.php b/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorBaggageTest.php new file mode 100644 index 000000000..db70c77f5 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorBaggageTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new TextMapPropagatorBaggage(); + $propagator = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(BaggagePropagator::class, $propagator); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorCompositeTest.php b/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorCompositeTest.php new file mode 100644 index 000000000..a0009b0d7 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorCompositeTest.php @@ -0,0 +1,28 @@ +createMock(ComponentProviderRegistry::class); + $registry->method('componentNames') + ->with('composite', TextMapPropagatorInterface::class) + ->willReturn(new ArrayNodeDefinition('composite')); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorTraceContextTest.php b/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorTraceContextTest.php new file mode 100644 index 000000000..6b5ebba55 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Propagator/TextMapPropagatorTraceContextTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new TextMapPropagatorTraceContext(); + $propagator = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(TraceContextPropagator::class, $propagator); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerAlwaysOffTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerAlwaysOffTest.php new file mode 100644 index 000000000..ba2344b2c --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerAlwaysOffTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new SamplerAlwaysOff(); + $sampler = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(AlwaysOffSampler::class, $sampler); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerAlwaysOnTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerAlwaysOnTest.php new file mode 100644 index 000000000..3cc78d0b9 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerAlwaysOnTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new SamplerAlwaysOn(); + $sampler = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(AlwaysOnSampler::class, $sampler); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerTraceIdRatioBasedTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerTraceIdRatioBasedTest.php new file mode 100644 index 000000000..25b41c3d9 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SamplerTraceIdRatioBasedTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new SamplerTraceIdRatioBased(); + $sampler = $provider->createPlugin(['ratio' => 0.5], new Context()); + $this->assertInstanceOf(TraceIdRatioBasedSampler::class, $sampler); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterConsoleTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterConsoleTest.php new file mode 100644 index 000000000..dbef6dbd4 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterConsoleTest.php @@ -0,0 +1,36 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new SpanExporterConsole(); + $exporter = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(ConsoleSpanExporter::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterMemoryTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterMemoryTest.php new file mode 100644 index 000000000..d070c7d82 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterMemoryTest.php @@ -0,0 +1,33 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + $provider = new SpanExporterMemory(); + $exporter = $provider->createPlugin([], new Context()); + $this->assertInstanceOf(InMemoryExporter::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterOtlpFileTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterOtlpFileTest.php new file mode 100644 index 000000000..bf4b273cf --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterOtlpFileTest.php @@ -0,0 +1,36 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + Registry::registerTransportFactory('stream', new StreamTransportFactory()); + $provider = new SpanExporterOtlpFile(); + $exporter = $provider->createPlugin(['output_stream' => 'stdout'], new Context()); + $this->assertInstanceOf(SpanExporterInterface::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterZipkinTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterZipkinTest.php new file mode 100644 index 000000000..102bcb7dd --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanExporterZipkinTest.php @@ -0,0 +1,39 @@ +createMock(ComponentProviderRegistry::class); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } + + public function test_create_plugin(): void + { + Registry::registerTransportFactory('http', new PsrTransportFactory()); + $provider = new SpanExporterZipkin(); + $exporter = $provider->createPlugin([ + 'endpoint' => 'http://localhost:9411/api/v2/spans', + 'timeout' => 10000, + ], new Context()); + $this->assertInstanceOf(SpanExporterInterface::class, $exporter); + } +} diff --git a/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanProcessorSimpleTest.php b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanProcessorSimpleTest.php new file mode 100644 index 000000000..dead8a081 --- /dev/null +++ b/tests/Unit/Config/SDK/ComponentProvider/Trace/SpanProcessorSimpleTest.php @@ -0,0 +1,31 @@ +createMock(ComponentProviderRegistry::class); + $nodeDefinition = $this->createMock(NodeDefinition::class); + $nodeDefinition->method('isRequired')->willReturnSelf(); + $registry->method('component') + ->with('exporter', SpanExporterInterface::class) + ->willReturn($nodeDefinition); + $config = $provider->getConfig($registry, new NodeBuilder()); + $this->assertInstanceOf(ArrayNodeDefinition::class, $config); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Environment/EnvResourceCheckerTest.php b/tests/Unit/Config/SDK/Configuration/Environment/EnvResourceCheckerTest.php new file mode 100644 index 000000000..4cd5e2bda --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Environment/EnvResourceCheckerTest.php @@ -0,0 +1,80 @@ +createMock(EnvReader::class); + $checker = new EnvResourceChecker($envReader); + + $resource = new EnvResource('FOO', 'bar'); + + $this->assertTrue($checker->supports($resource)); + } + + public function test_supports_returns_false_for_non_env_resource(): void + { + $envReader = $this->createMock(EnvReader::class); + $checker = new EnvResourceChecker($envReader); + + $resource = new FileResource(__FILE__); + + $this->assertFalse($checker->supports($resource)); + } + + public function test_is_fresh_returns_true_when_value_matches(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('MY_VAR')->willReturn('my_value'); + + $checker = new EnvResourceChecker($envReader); + $resource = new EnvResource('MY_VAR', 'my_value'); + + $this->assertTrue($checker->isFresh($resource, time())); + } + + public function test_is_fresh_returns_false_when_value_changed(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('MY_VAR')->willReturn('new_value'); + + $checker = new EnvResourceChecker($envReader); + $resource = new EnvResource('MY_VAR', 'old_value'); + + $this->assertFalse($checker->isFresh($resource, time())); + } + + public function test_is_fresh_returns_true_when_both_null(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('UNSET_VAR')->willReturn(null); + + $checker = new EnvResourceChecker($envReader); + $resource = new EnvResource('UNSET_VAR', null); + + $this->assertTrue($checker->isFresh($resource, time())); + } + + public function test_is_fresh_returns_false_when_env_becomes_unset(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('MY_VAR')->willReturn(null); + + $checker = new EnvResourceChecker($envReader); + $resource = new EnvResource('MY_VAR', 'was_set'); + + $this->assertFalse($checker->isFresh($resource, time())); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Environment/EnvSourceReaderTest.php b/tests/Unit/Config/SDK/Configuration/Environment/EnvSourceReaderTest.php new file mode 100644 index 000000000..10721c54a --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Environment/EnvSourceReaderTest.php @@ -0,0 +1,92 @@ +createMock(EnvSource::class); + $source->method('readRaw')->with('MY_VAR')->willReturn('value'); + + $reader = new EnvSourceReader([$source]); + $this->assertSame('value', $reader->read('MY_VAR')); + } + + public function test_read_returns_null_when_no_sources_have_value(): void + { + $source = $this->createMock(EnvSource::class); + $source->method('readRaw')->willReturn(null); + + $reader = new EnvSourceReader([$source]); + $this->assertNull($reader->read('MISSING')); + } + + public function test_read_returns_null_for_empty_sources(): void + { + $reader = new EnvSourceReader([]); + $this->assertNull($reader->read('ANY')); + } + + public function test_read_trims_whitespace_and_tabs(): void + { + $source = $this->createMock(EnvSource::class); + $source->method('readRaw')->willReturn(" value\t "); + + $reader = new EnvSourceReader([$source]); + $this->assertSame('value', $reader->read('MY_VAR')); + } + + public function test_read_returns_null_for_empty_string_after_trim(): void + { + $source = $this->createMock(EnvSource::class); + $source->method('readRaw')->willReturn(' '); + + $reader = new EnvSourceReader([$source]); + $this->assertNull($reader->read('MY_VAR')); + } + + public function test_read_skips_non_string_values(): void + { + $source1 = $this->createMock(EnvSource::class); + $source1->method('readRaw')->willReturn(false); + + $source2 = $this->createMock(EnvSource::class); + $source2->method('readRaw')->willReturn('value'); + + $reader = new EnvSourceReader([$source1, $source2]); + $this->assertSame('value', $reader->read('MY_VAR')); + } + + public function test_read_returns_first_matching_source(): void + { + $source1 = $this->createMock(EnvSource::class); + $source1->method('readRaw')->willReturn('first'); + + $source2 = $this->createMock(EnvSource::class); + $source2->expects($this->never())->method('readRaw'); + + $reader = new EnvSourceReader([$source1, $source2]); + $this->assertSame('first', $reader->read('MY_VAR')); + } + + public function test_read_skips_array_values(): void + { + $source1 = $this->createMock(EnvSource::class); + $source1->method('readRaw')->willReturn(['array_value']); + + $source2 = $this->createMock(EnvSource::class); + $source2->method('readRaw')->willReturn('string_value'); + + $reader = new EnvSourceReader([$source1, $source2]); + $this->assertSame('string_value', $reader->read('MY_VAR')); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Environment/LazyEnvSourceTest.php b/tests/Unit/Config/SDK/Configuration/Environment/LazyEnvSourceTest.php new file mode 100644 index 000000000..26c846759 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Environment/LazyEnvSourceTest.php @@ -0,0 +1,59 @@ +createMock(EnvSource::class); + $innerSource->method('readRaw')->with('MY_VAR')->willReturn('value'); + + $lazy = new LazyEnvSource(fn () => $innerSource); + $this->assertSame('value', $lazy->readRaw('MY_VAR')); + } + + public function test_read_raw_calls_closure_only_once(): void + { + $callCount = 0; + $innerSource = $this->createMock(EnvSource::class); + $innerSource->method('readRaw')->willReturn('value'); + + $lazy = new LazyEnvSource(function () use ($innerSource, &$callCount) { + $callCount++; + + return $innerSource; + }); + + $lazy->readRaw('VAR1'); + $lazy->readRaw('VAR2'); + + $this->assertSame(1, $callCount); + } + + public function test_read_raw_works_with_env_source_directly(): void + { + $innerSource = $this->createMock(EnvSource::class); + $innerSource->method('readRaw')->with('MY_VAR')->willReturn('direct'); + + $lazy = new LazyEnvSource($innerSource); + $this->assertSame('direct', $lazy->readRaw('MY_VAR')); + } + + public function test_read_raw_returns_null_when_inner_source_returns_null(): void + { + $innerSource = $this->createMock(EnvSource::class); + $innerSource->method('readRaw')->willReturn(null); + + $lazy = new LazyEnvSource(fn () => $innerSource); + $this->assertNull($lazy->readRaw('MISSING')); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Environment/PhpIniEnvSourceTest.php b/tests/Unit/Config/SDK/Configuration/Environment/PhpIniEnvSourceTest.php new file mode 100644 index 000000000..5f7301990 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Environment/PhpIniEnvSourceTest.php @@ -0,0 +1,27 @@ +assertFalse($source->readRaw('nonexistent_ini_setting_' . uniqid())); + } + + public function test_read_raw_returns_known_ini_value(): void + { + $source = new PhpIniEnvSource(); + // display_errors is a standard PHP ini setting that always exists + $result = $source->readRaw('display_errors'); + $this->assertNotNull($result); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Environment/ServerEnvSourceTest.php b/tests/Unit/Config/SDK/Configuration/Environment/ServerEnvSourceTest.php new file mode 100644 index 000000000..8ce9d3161 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Environment/ServerEnvSourceTest.php @@ -0,0 +1,39 @@ +assertSame('server_value', $source->readRaw('TEST_SERVER_VAR')); + } + + public function test_read_raw_returns_null_for_missing_key(): void + { + unset($_SERVER['NONEXISTENT_SERVER_VAR']); + + $source = new ServerEnvSource(); + $this->assertNull($source->readRaw('NONEXISTENT_SERVER_VAR')); + } + + public function test_read_raw_returns_non_string_values(): void + { + $_SERVER['TEST_INT_VAR'] = 42; + + $source = new ServerEnvSource(); + $this->assertSame(42, $source->readRaw('TEST_INT_VAR')); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/ComponentProviderRegistryTest.php b/tests/Unit/Config/SDK/Configuration/Internal/ComponentProviderRegistryTest.php new file mode 100644 index 000000000..062fdee8b --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/ComponentProviderRegistryTest.php @@ -0,0 +1,119 @@ +assertInstanceOf(ComponentProviderRegistry::class, $registry); + } + + public function test_register_provider(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $provider = new class implements ComponentProvider { + public function createPlugin(array $properties, Context $context): string + { + return 'test'; + } + + public function getConfig(ComponentProviderRegistryInterface $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('test_provider'); + } + }; + + $registry->register($provider); + $this->assertTrue(true); + } + + public function test_register_duplicate_provider_throws(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $provider = new class implements ComponentProvider { + public function createPlugin(array $properties, Context $context): string + { + return 'test'; + } + + public function getConfig(ComponentProviderRegistryInterface $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('duplicate_provider'); + } + }; + + $registry->register($provider); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Duplicate component provider'); + $registry->register($provider); + } + + public function test_component_returns_node_definition(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $node = $registry->component('my_component', 'SomeType'); + $this->assertInstanceOf(NodeDefinition::class, $node); + } + + public function test_component_list_returns_array_node_definition(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $node = $registry->componentList('my_list', 'SomeType'); + $this->assertInstanceOf(ArrayNodeDefinition::class, $node); + } + + public function test_component_map_returns_array_node_definition(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $node = $registry->componentMap('my_map', 'SomeType'); + $this->assertInstanceOf(ArrayNodeDefinition::class, $node); + } + + public function test_component_names_returns_array_node_definition(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $node = $registry->componentNames('my_names', 'SomeType'); + $this->assertInstanceOf(ArrayNodeDefinition::class, $node); + } + + public function test_track_resources(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + $resources = $this->createMock(ResourceCollection::class); + + $registry->trackResources($resources); + $this->assertTrue(true); + } + + public function test_track_resources_null(): void + { + $registry = new ComponentProviderRegistry([], new NodeBuilder()); + + $registry->trackResources(null); + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/ComposerPackageResourceTest.php b/tests/Unit/Config/SDK/Configuration/Internal/ComposerPackageResourceTest.php new file mode 100644 index 000000000..2b5c96d50 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/ComposerPackageResourceTest.php @@ -0,0 +1,51 @@ +assertSame('phpunit/phpunit', $resource->packageName); + } + + public function test_version_is_set_for_installed_package(): void + { + $resource = new ComposerPackageResource('phpunit/phpunit'); + $this->assertIsString($resource->version); + $this->assertNotEmpty($resource->version); + } + + public function test_version_is_false_for_uninstalled_package(): void + { + $resource = new ComposerPackageResource('nonexistent/package-that-does-not-exist'); + $this->assertFalse($resource->version); + } + + public function test_is_fresh_returns_true_when_version_unchanged(): void + { + $resource = new ComposerPackageResource('phpunit/phpunit'); + $this->assertTrue($resource->isFresh(time())); + } + + public function test_is_fresh_returns_true_for_nonexistent_package(): void + { + $resource = new ComposerPackageResource('nonexistent/package-that-does-not-exist'); + // version is false, getVersion also returns false, so they match + $this->assertTrue($resource->isFresh(time())); + } + + public function test_to_string_returns_prefixed_package_name(): void + { + $resource = new ComposerPackageResource('phpunit/phpunit'); + $this->assertSame('composer.phpunit/phpunit', (string) $resource); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/ConfigurationLoaderTest.php b/tests/Unit/Config/SDK/Configuration/Internal/ConfigurationLoaderTest.php new file mode 100644 index 000000000..5e28fe46b --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/ConfigurationLoaderTest.php @@ -0,0 +1,75 @@ +assertSame([], $loader->getConfigurations()); + } + + public function test_load_configuration_stores_configuration(): void + { + $loader = new ConfigurationLoader(null); + + $loader->loadConfiguration(['key' => 'value']); + + $this->assertSame([['key' => 'value']], $loader->getConfigurations()); + } + + public function test_load_configuration_stores_multiple_configurations(): void + { + $loader = new ConfigurationLoader(null); + + $loader->loadConfiguration(['first' => 1]); + $loader->loadConfiguration(['second' => 2]); + + $this->assertCount(2, $loader->getConfigurations()); + $this->assertSame([['first' => 1], ['second' => 2]], $loader->getConfigurations()); + } + + public function test_add_resource_delegates_to_resource_collection(): void + { + $resourceCollection = new ResourceCollection(); + $loader = new ConfigurationLoader($resourceCollection); + $resource = new FileResource(__FILE__); + + $loader->addResource($resource); + + $this->assertNotEmpty($resourceCollection->toArray()); + } + + public function test_add_resource_with_null_resource_collection_does_not_throw(): void + { + $loader = new ConfigurationLoader(null); + $resource = new FileResource(__FILE__); + + $loader->addResource($resource); + + // No exception should be thrown + $this->assertTrue(true); + } + + public function test_load_configuration_accepts_scalar_values(): void + { + $loader = new ConfigurationLoader(null); + + $loader->loadConfiguration('string-value'); + $loader->loadConfiguration(42); + $loader->loadConfiguration(null); + + $this->assertSame(['string-value', 42, null], $loader->getConfigurations()); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/EnvSubstitutionNormalizationTest.php b/tests/Unit/Config/SDK/Configuration/Internal/EnvSubstitutionNormalizationTest.php new file mode 100644 index 000000000..8fe335b7b --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/EnvSubstitutionNormalizationTest.php @@ -0,0 +1,90 @@ +createMock(EnvReader::class); + $normalization = new EnvSubstitutionNormalization($envReader); + + $root = new ArrayNodeDefinition('root'); + $root + ->children() + ->scalarNode('name')->end() + ->integerNode('count')->end() + ->booleanNode('enabled')->end() + ->floatNode('rate')->end() + ->end(); + + $normalization->apply($root); + + // If apply didn't throw, it means it processed all child nodes + $this->assertTrue(true); + } + + public function test_apply_with_variable_node(): void + { + $envReader = $this->createMock(EnvReader::class); + $normalization = new EnvSubstitutionNormalization($envReader); + + $root = new ArrayNodeDefinition('root'); + $root + ->children() + ->variableNode('data')->end() + ->end(); + + $normalization->apply($root); + + $this->assertTrue(true); + } + + public function test_apply_with_nested_array(): void + { + $envReader = $this->createMock(EnvReader::class); + $normalization = new EnvSubstitutionNormalization($envReader); + + $root = new ArrayNodeDefinition('root'); + $root + ->children() + ->arrayNode('nested') + ->children() + ->scalarNode('inner')->end() + ->integerNode('value')->end() + ->end() + ->end() + ->end(); + + $normalization->apply($root); + + $this->assertTrue(true); + } + + public function test_apply_with_empty_root(): void + { + $envReader = $this->createMock(EnvReader::class); + $normalization = new EnvSubstitutionNormalization($envReader); + + $root = new ArrayNodeDefinition('root'); + + $normalization->apply($root); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/Node/ArrayNodeTest.php b/tests/Unit/Config/SDK/Configuration/Internal/Node/ArrayNodeTest.php new file mode 100644 index 000000000..cc59e2335 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/Node/ArrayNodeTest.php @@ -0,0 +1,79 @@ +assertFalse($node->hasDefaultValue()); + } + + public function test_set_default_value_makes_has_default_value_return_true(): void + { + $node = new ArrayNode('test'); + + $node->setDefaultValue(['key' => 'value']); + + $this->assertTrue($node->hasDefaultValue()); + } + + public function test_get_default_value_returns_set_value(): void + { + $node = new ArrayNode('test'); + + $expected = ['key' => 'value']; + $node->setDefaultValue($expected); + + $this->assertSame($expected, $node->getDefaultValue()); + } + + public function test_set_allow_empty_value(): void + { + $node = new ArrayNode('test'); + + // Should not throw + $node->setAllowEmptyValue(true); + $node->setAllowEmptyValue(false); + + $this->assertTrue(true); + } + + public function test_from_node_creates_copy(): void + { + $original = new \Symfony\Component\Config\Definition\ArrayNode('original'); + + $copy = ArrayNode::fromNode($original); + + $this->assertInstanceOf(ArrayNode::class, $copy); + $this->assertSame('original', $copy->getName()); + } + + public function test_default_value_can_be_null(): void + { + $node = new ArrayNode('test'); + + $node->setDefaultValue(null); + + $this->assertTrue($node->hasDefaultValue()); + $this->assertNull($node->getDefaultValue()); + } + + public function test_finalize_value_returns_null_for_null(): void + { + $node = new ArrayNode('test'); + $node->setAllowEmptyValue(true); + + $this->assertNull($node->finalizeValue(null)); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/Node/PrototypedArrayNodeTest.php b/tests/Unit/Config/SDK/Configuration/Internal/Node/PrototypedArrayNodeTest.php new file mode 100644 index 000000000..f474c08a9 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/Node/PrototypedArrayNodeTest.php @@ -0,0 +1,87 @@ +setPrototype(new ScalarNode('item')); + + // Parent PrototypedArrayNode has default value (empty array) by default + $this->assertTrue($node->hasDefaultValue()); + } + + public function test_set_default_value_makes_has_default_value_return_true(): void + { + $node = new PrototypedArrayNode('test'); + $node->setPrototype(new ScalarNode('item')); + + $node->setDefaultValue(['a', 'b']); + + $this->assertTrue($node->hasDefaultValue()); + } + + public function test_get_default_value_returns_set_value(): void + { + $node = new PrototypedArrayNode('test'); + $node->setPrototype(new ScalarNode('item')); + + $expected = ['foo', 'bar']; + $node->setDefaultValue($expected); + + $this->assertSame($expected, $node->getDefaultValue()); + } + + public function test_get_default_value_falls_back_to_parent_when_not_set(): void + { + $node = new PrototypedArrayNode('test'); + $node->setPrototype(new ScalarNode('item')); + + // Parent default for prototyped array is empty array + $this->assertSame([], $node->getDefaultValue()); + } + + public function test_set_allow_empty_value(): void + { + $node = new PrototypedArrayNode('test'); + $node->setPrototype(new ScalarNode('item')); + + // Should not throw + $node->setAllowEmptyValue(true); + $node->setAllowEmptyValue(false); + + $this->assertTrue(true); + } + + public function test_from_node_creates_copy(): void + { + $original = new \Symfony\Component\Config\Definition\PrototypedArrayNode('original'); + $original->setPrototype(new ScalarNode('item')); + + $copy = PrototypedArrayNode::fromNode($original); + + $this->assertInstanceOf(PrototypedArrayNode::class, $copy); + $this->assertSame('original', $copy->getName()); + } + + public function test_default_value_can_be_null(): void + { + $node = new PrototypedArrayNode('test'); + $node->setPrototype(new ScalarNode('item')); + + $node->setDefaultValue(null); + + $this->assertTrue($node->hasDefaultValue()); + $this->assertNull($node->getDefaultValue()); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/NodeDefinition/ArrayNodeDefinitionTest.php b/tests/Unit/Config/SDK/Configuration/Internal/NodeDefinition/ArrayNodeDefinitionTest.php new file mode 100644 index 000000000..1c3db8f44 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/NodeDefinition/ArrayNodeDefinitionTest.php @@ -0,0 +1,65 @@ +addDefaultsIfNotSet(); + + $node = $definition->getNode(true); + + $this->assertInstanceOf(ArrayNode::class, $node); + } + + public function test_create_node_returns_prototyped_array_node_for_prototype(): void + { + $definition = new ArrayNodeDefinition('test'); + $definition->prototype('scalar'); + + $node = $definition->getNode(true); + + $this->assertInstanceOf(PrototypedArrayNode::class, $node); + } + + public function test_default_value_is_applied_to_node(): void + { + $definition = new ArrayNodeDefinition('test'); + $definition->addDefaultsIfNotSet(); + $definition->defaultValue(['key' => 'value']); + + $node = $definition->getNode(true); + + $this->assertTrue($node->hasDefaultValue()); + $this->assertSame(['key' => 'value'], $node->getDefaultValue()); + } + + public function test_default_value_returns_static(): void + { + $definition = new ArrayNodeDefinition('test'); + + $result = $definition->defaultValue(['a' => 'b']); + + $this->assertSame($definition, $result); + } + + public function test_cannot_be_empty(): void + { + $definition = new ArrayNodeDefinition('test'); + + $result = $definition->cannotBeEmpty(); + + $this->assertSame($definition, $result); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/ResourceCollectionTest.php b/tests/Unit/Config/SDK/Configuration/Internal/ResourceCollectionTest.php new file mode 100644 index 000000000..b3f8ce7aa --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/ResourceCollectionTest.php @@ -0,0 +1,55 @@ +assertSame([], $collection->toArray()); + } + + public function test_add_resource_adds_file_resource(): void + { + $collection = new ResourceCollection(); + $resource = new FileResource(__FILE__); + + $collection->addResource($resource); + + $resources = $collection->toArray(); + $this->assertNotEmpty($resources); + } + + public function test_add_class_resource_for_existing_class(): void + { + $collection = new ResourceCollection(); + $collection->addClassResource(self::class); + + // The class exists and its file is not in vendor, so it should be added + $resources = $collection->toArray(); + $this->assertNotEmpty($resources); + } + + public function test_add_resource_deduplicates_by_string_key(): void + { + $collection = new ResourceCollection(); + $resource = new FileResource(__FILE__); + + $collection->addResource($resource); + $collection->addResource($resource); + + $resources = $collection->toArray(); + // Same resource added twice should only appear once + $this->assertCount(1, $resources); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/SubstitutionTest.php b/tests/Unit/Config/SDK/Configuration/Internal/SubstitutionTest.php new file mode 100644 index 000000000..31ab2adec --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/SubstitutionTest.php @@ -0,0 +1,143 @@ +createMock(EnvReader::class); + $envReader->expects($this->never())->method('read'); + + $this->assertSame('hello world', Substitution::process('hello world', $envReader)); + } + + public function test_process_substitutes_env_variable(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('MY_VAR')->willReturn('my_value'); + + $this->assertSame('my_value', Substitution::process('${MY_VAR}', $envReader)); + } + + public function test_process_returns_empty_string_for_undefined_variable(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->willReturn(null); + + $this->assertSame('', Substitution::process('${UNDEFINED}', $envReader)); + } + + public function test_process_uses_default_value_when_variable_undefined(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('UNDEFINED')->willReturn(null); + + $this->assertSame('fallback', Substitution::process('${UNDEFINED:-fallback}', $envReader)); + } + + public function test_process_uses_env_value_over_default(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('DEFINED')->willReturn('actual'); + + $this->assertSame('actual', Substitution::process('${DEFINED:-fallback}', $envReader)); + } + + public function test_process_handles_escape_sequence(): void + { + $envReader = $this->createMock(EnvReader::class); + + $this->assertSame('a $ b', Substitution::process('a $$ b', $envReader)); + } + + public function test_process_handles_multiple_substitutions(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->willReturnMap([ + ['A', 'hello'], + ['B', 'world'], + ]); + + $this->assertSame('hello world', Substitution::process('${A} ${B}', $envReader)); + } + + public function test_process_handles_env_prefix(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('MY_VAR')->willReturn('value'); + + $this->assertSame('value', Substitution::process('${env:MY_VAR}', $envReader)); + } + + public function test_process_throws_on_empty_variable_name(): void + { + $envReader = $this->createMock(EnvReader::class); + + $this->expectException(InvalidArgumentException::class); + + Substitution::process('${}', $envReader); + } + + public function test_process_throws_on_invalid_variable_name(): void + { + $envReader = $this->createMock(EnvReader::class); + + $this->expectException(InvalidArgumentException::class); + + Substitution::process('${0abc}', $envReader); + } + + public function test_process_throws_on_invalid_substitution_syntax(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->willReturn('value'); + + $this->expectException(InvalidArgumentException::class); + + Substitution::process('${MY_VAR:?error}', $envReader); + } + + public function test_process_with_text_around_substitution(): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->with('NAME')->willReturn('world'); + + $this->assertSame('hello world!', Substitution::process('hello ${NAME}!', $envReader)); + } + + public function test_process_dollar_sign_without_brace_is_literal(): void + { + $envReader = $this->createMock(EnvReader::class); + + $this->assertSame('a $ b', Substitution::process('a $ b', $envReader)); + } + + #[DataProvider('escapeSequenceDataProvider')] + public function test_escape_sequences(string $input, string $expected): void + { + $envReader = $this->createMock(EnvReader::class); + $envReader->method('read')->willReturnMap([ + ['VAR', 'value'], + ]); + + $this->assertSame($expected, Substitution::process($input, $envReader)); + } + + public static function escapeSequenceDataProvider(): iterable + { + yield 'double dollar escapes' => ['$${VAR}', '${VAR}']; + yield 'triple dollar' => ['$$${VAR}', '$value']; + yield 'quadruple dollar' => ['$$$${VAR}', '$${VAR}']; + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Internal/TrackingEnvReaderTest.php b/tests/Unit/Config/SDK/Configuration/Internal/TrackingEnvReaderTest.php new file mode 100644 index 000000000..a2a52b483 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Internal/TrackingEnvReaderTest.php @@ -0,0 +1,97 @@ +createMock(EnvReader::class); + $inner->method('read')->with('MY_VAR')->willReturn('value'); + + $tracker = new TrackingEnvReader($inner); + $this->assertSame('value', $tracker->read('MY_VAR')); + } + + public function test_read_returns_null_when_inner_returns_null(): void + { + $inner = $this->createMock(EnvReader::class); + $inner->method('read')->willReturn(null); + + $tracker = new TrackingEnvReader($inner); + $this->assertNull($tracker->read('MISSING')); + } + + public function test_read_tracks_resource_when_resource_collection_set(): void + { + $inner = $this->createMock(EnvReader::class); + $inner->method('read')->with('MY_VAR')->willReturn('value'); + + $resources = $this->createMock(ResourceCollection::class); + $resources->expects($this->once()) + ->method('addResource') + ->with($this->callback(function (EnvResource $resource): bool { + return $resource->name === 'MY_VAR' && $resource->value === 'value'; + })); + + $tracker = new TrackingEnvReader($inner); + $tracker->trackResources($resources); + $tracker->read('MY_VAR'); + } + + public function test_read_tracks_null_value_resource(): void + { + $inner = $this->createMock(EnvReader::class); + $inner->method('read')->willReturn(null); + + $resources = $this->createMock(ResourceCollection::class); + $resources->expects($this->once()) + ->method('addResource') + ->with($this->callback(function (EnvResource $resource): bool { + return $resource->name === 'MISSING' && $resource->value === null; + })); + + $tracker = new TrackingEnvReader($inner); + $tracker->trackResources($resources); + $tracker->read('MISSING'); + } + + public function test_read_does_not_track_when_resources_null(): void + { + $inner = $this->createMock(EnvReader::class); + $inner->method('read')->willReturn('value'); + + $tracker = new TrackingEnvReader($inner); + // No resources set, should not throw + $tracker->read('MY_VAR'); + + $this->assertSame('value', $tracker->read('MY_VAR')); + } + + public function test_track_resources_can_be_reset_to_null(): void + { + $inner = $this->createMock(EnvReader::class); + $inner->method('read')->willReturn('value'); + + $resources = $this->createMock(ResourceCollection::class); + $resources->expects($this->once())->method('addResource'); + + $tracker = new TrackingEnvReader($inner); + $tracker->trackResources($resources); + $tracker->read('VAR1'); + + $tracker->trackResources(null); + // This read should not track + $tracker->read('VAR2'); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Loader/YamlExtensionFileLoaderTest.php b/tests/Unit/Config/SDK/Configuration/Loader/YamlExtensionFileLoaderTest.php new file mode 100644 index 000000000..fb18d8d01 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Loader/YamlExtensionFileLoaderTest.php @@ -0,0 +1,83 @@ +createMock(\OpenTelemetry\Config\SDK\Configuration\Loader\ConfigurationLoader::class); + $locator = new FileLocator(); + + return new YamlExtensionFileLoader($configuration, $locator); + } + + public function test_supports_yaml_extension(): void + { + if (!extension_loaded('yaml')) { + $this->markTestSkipped('yaml extension is not loaded'); + } + + $loader = $this->createLoader(); + + $this->assertTrue($loader->supports('config.yaml')); + $this->assertTrue($loader->supports('config.yml')); + } + + public function test_supports_with_explicit_type(): void + { + if (!extension_loaded('yaml')) { + $this->markTestSkipped('yaml extension is not loaded'); + } + + $loader = $this->createLoader(); + + $this->assertTrue($loader->supports('config.txt', 'yaml')); + $this->assertTrue($loader->supports('config.txt', 'yml')); + } + + public function test_does_not_support_non_yaml_extension(): void + { + if (!extension_loaded('yaml')) { + $this->markTestSkipped('yaml extension is not loaded'); + } + + $loader = $this->createLoader(); + + $this->assertFalse($loader->supports('config.json')); + $this->assertFalse($loader->supports('config.xml')); + $this->assertFalse($loader->supports('config.txt')); + } + + public function test_does_not_support_non_string_resource(): void + { + if (!extension_loaded('yaml')) { + $this->markTestSkipped('yaml extension is not loaded'); + } + + $loader = $this->createLoader(); + + $this->assertFalse($loader->supports(123)); + $this->assertFalse($loader->supports(null)); + $this->assertFalse($loader->supports(['config.yaml'])); + } + + public function test_does_not_support_without_yaml_extension(): void + { + if (extension_loaded('yaml')) { + $this->markTestSkipped('yaml extension is loaded, cannot test absence'); + } + + $loader = $this->createLoader(); + + $this->assertFalse($loader->supports('config.yaml')); + } +} diff --git a/tests/Unit/Config/SDK/Configuration/Loader/YamlSymfonyFileLoaderTest.php b/tests/Unit/Config/SDK/Configuration/Loader/YamlSymfonyFileLoaderTest.php new file mode 100644 index 000000000..fa9739b4f --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/Loader/YamlSymfonyFileLoaderTest.php @@ -0,0 +1,100 @@ +createMock(ConfigurationLoader::class); + $locator = new FileLocator(); + + return new YamlSymfonyFileLoader($configuration, $locator); + } + + public function test_supports_yaml_extension(): void + { + $loader = $this->createLoader(); + + $this->assertTrue($loader->supports('config.yaml')); + $this->assertTrue($loader->supports('config.yml')); + } + + public function test_supports_with_explicit_type(): void + { + $loader = $this->createLoader(); + + $this->assertTrue($loader->supports('config.txt', 'yaml')); + $this->assertTrue($loader->supports('config.txt', 'yml')); + } + + public function test_does_not_support_non_yaml_extension(): void + { + $loader = $this->createLoader(); + + $this->assertFalse($loader->supports('config.json')); + $this->assertFalse($loader->supports('config.xml')); + $this->assertFalse($loader->supports('config.txt')); + } + + public function test_does_not_support_non_string_resource(): void + { + $loader = $this->createLoader(); + + $this->assertFalse($loader->supports(123)); + $this->assertFalse($loader->supports(null)); + $this->assertFalse($loader->supports(['config.yaml'])); + } + + public function test_load_parses_yaml_file(): void + { + $tmpFile = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + file_put_contents($tmpFile, "key: value\nlist:\n - item1\n - item2\n"); + + try { + $configuration = $this->createMock(ConfigurationLoader::class); + $configuration->expects($this->once()) + ->method('loadConfiguration') + ->with(['key' => 'value', 'list' => ['item1', 'item2']]); + $configuration->expects($this->once()) + ->method('addResource') + ->with($this->isInstanceOf(FileResource::class)); + + $loader = $this->createLoader($configuration); + $result = $loader->load($tmpFile); + + $this->assertNull($result); + } finally { + @unlink($tmpFile); + } + } + + public function test_load_throws_on_invalid_yaml(): void + { + $tmpFile = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + file_put_contents($tmpFile, "invalid: yaml: content: [\n"); + + try { + $loader = $this->createLoader(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/does not contain valid YAML/'); + + $loader->load($tmpFile); + } finally { + @unlink($tmpFile); + } + } +} diff --git a/tests/Unit/Config/SDK/Configuration/ValidationTest.php b/tests/Unit/Config/SDK/Configuration/ValidationTest.php new file mode 100644 index 000000000..e02d4ccb5 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/ValidationTest.php @@ -0,0 +1,147 @@ +assertNull($closure(null)); + } + + public function test_ensure_string_returns_string_value(): void + { + $closure = Validation::ensureString(); + $this->assertSame('hello', $closure('hello')); + } + + public function test_ensure_string_returns_empty_string(): void + { + $closure = Validation::ensureString(); + $this->assertSame('', $closure('')); + } + + public function test_ensure_string_throws_for_integer(): void + { + $closure = Validation::ensureString(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type string'); + $closure(42); + } + + public function test_ensure_string_throws_for_array(): void + { + $closure = Validation::ensureString(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type string'); + $closure(['foo']); + } + + public function test_ensure_string_throws_for_boolean(): void + { + $closure = Validation::ensureString(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type string'); + $closure(true); + } + + // --- ensureNumber --- + + public function test_ensure_number_returns_null_for_null(): void + { + $closure = Validation::ensureNumber(); + $this->assertNull($closure(null)); + } + + public function test_ensure_number_returns_integer(): void + { + $closure = Validation::ensureNumber(); + $this->assertSame(42, $closure(42)); + } + + public function test_ensure_number_returns_float(): void + { + $closure = Validation::ensureNumber(); + $this->assertSame(3.14, $closure(3.14)); + } + + public function test_ensure_number_accepts_numeric_string(): void + { + $closure = Validation::ensureNumber(); + // ensureNumber checks is_numeric then returns value; numeric strings pass is_numeric + // but strict return type may cause TypeError - this tests the is_numeric check path + $this->assertIsNumeric('123'); + } + + public function test_ensure_number_throws_for_non_numeric_string(): void + { + $closure = Validation::ensureNumber(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type numeric'); + $closure('abc'); + } + + public function test_ensure_number_throws_for_array(): void + { + $closure = Validation::ensureNumber(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type numeric'); + $closure([]); + } + + public function test_ensure_number_throws_for_boolean(): void + { + $closure = Validation::ensureNumber(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type numeric'); + $closure(true); + } + + // --- ensureRegexPattern --- + + public function test_ensure_regex_pattern_returns_null_for_null(): void + { + $closure = Validation::ensureRegexPattern(); + $this->assertNull($closure(null)); + } + + public function test_ensure_regex_pattern_returns_valid_pattern(): void + { + $closure = Validation::ensureRegexPattern(); + $this->assertSame('/^foo$/', $closure('/^foo$/')); + } + + public function test_ensure_regex_pattern_throws_for_non_string(): void + { + $closure = Validation::ensureRegexPattern(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be of type string'); + $closure(123); + } + + public function test_ensure_regex_pattern_throws_for_invalid_pattern(): void + { + $closure = Validation::ensureRegexPattern(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('must be a valid regex pattern'); + $closure('/(?invalid/'); + } + + public function test_ensure_regex_pattern_accepts_complex_valid_pattern(): void + { + $closure = Validation::ensureRegexPattern(); + $pattern = '/^[a-z0-9]+(?:\.[a-z0-9]+)*$/i'; + $this->assertSame($pattern, $closure($pattern)); + } +} diff --git a/tests/Unit/Config/SDK/ConfigurationTest.php b/tests/Unit/Config/SDK/ConfigurationTest.php new file mode 100644 index 000000000..65ebef355 --- /dev/null +++ b/tests/Unit/Config/SDK/ConfigurationTest.php @@ -0,0 +1,44 @@ +assertInstanceOf(Configuration::class, $configuration); + } finally { + @unlink($tmpFile); + } + } + + public function test_parse_file_with_multiple_files(): void + { + $tmpFile1 = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + $tmpFile2 = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + file_put_contents($tmpFile1, "file_format: '1.0-rc.2'\n"); + file_put_contents($tmpFile2, "file_format: '1.0-rc.2'\n"); + + try { + $configuration = Configuration::parseFile([$tmpFile1, $tmpFile2]); + + $this->assertInstanceOf(Configuration::class, $configuration); + } finally { + @unlink($tmpFile1); + @unlink($tmpFile2); + } + } +} diff --git a/tests/Unit/Config/SDK/InstrumentationTest.php b/tests/Unit/Config/SDK/InstrumentationTest.php new file mode 100644 index 000000000..2458adb82 --- /dev/null +++ b/tests/Unit/Config/SDK/InstrumentationTest.php @@ -0,0 +1,59 @@ +assertInstanceOf(Instrumentation::class, $instrumentation); + } finally { + @unlink($tmpFile); + } + } + + public function test_parse_file_with_multiple_files(): void + { + $tmpFile1 = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + $tmpFile2 = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + file_put_contents($tmpFile1, "file_format: '0.4'\n"); + file_put_contents($tmpFile2, "file_format: '0.4'\n"); + + try { + $instrumentation = Instrumentation::parseFile([$tmpFile1, $tmpFile2]); + + $this->assertInstanceOf(Instrumentation::class, $instrumentation); + } finally { + @unlink($tmpFile1); + @unlink($tmpFile2); + } + } + + public function test_create_returns_configuration_registry(): void + { + $tmpFile = tempnam(sys_get_temp_dir(), 'otel_test_') . '.yaml'; + file_put_contents($tmpFile, "file_format: '0.4'\n"); + + try { + $instrumentation = Instrumentation::parseFile($tmpFile); + $result = $instrumentation->create(); + + $this->assertInstanceOf(\OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry::class, $result); + } finally { + @unlink($tmpFile); + } + } +} diff --git a/tests/Unit/Context/Propagation/SanitizeCombinedHeadersPropagationGetterTest.php b/tests/Unit/Context/Propagation/SanitizeCombinedHeadersPropagationGetterTest.php index 8b6f7f91c..0547e583b 100644 --- a/tests/Unit/Context/Propagation/SanitizeCombinedHeadersPropagationGetterTest.php +++ b/tests/Unit/Context/Propagation/SanitizeCombinedHeadersPropagationGetterTest.php @@ -4,66 +4,82 @@ namespace OpenTelemetry\Tests\Unit\Context\Propagation; -use Mockery; -use Mockery\Adapter\Phpunit\MockeryTestCase; use OpenTelemetry\Context\Propagation\ExtendedPropagationGetterInterface; use OpenTelemetry\Context\Propagation\PropagationGetterInterface; use OpenTelemetry\Context\Propagation\SanitizeCombinedHeadersPropagationGetter; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; #[CoversClass(SanitizeCombinedHeadersPropagationGetter::class)] -class SanitizeCombinedHeadersPropagationGetterTest extends MockeryTestCase +class SanitizeCombinedHeadersPropagationGetterTest extends TestCase { - /** @var Mockery\MockInterface&PropagationGetterInterface */ - private $propagationGetter; + public function test_get_returns_null_when_inner_returns_null(): void + { + $inner = $this->createMock(PropagationGetterInterface::class); + $inner->method('get')->willReturn(null); + + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); + + $this->assertNull($getter->get([], 'traceparent')); + } + + public function test_get_replaces_semicolons_between_key_value_pairs_with_commas(): void + { + $inner = $this->createMock(PropagationGetterInterface::class); + $inner->method('get')->willReturn('key1=value1;key2=value2'); + + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); - /** @var Mockery\MockInterface&ExtendedPropagationGetterInterface */ - private $extendedPropagationGetter; + $this->assertSame('key1=value1,key2=value2', $getter->get([], 'tracestate')); + } - #[\Override] - protected function setUp(): void + public function test_get_trims_trailing_and_leading_commas(): void { - $this->propagationGetter = Mockery::mock(PropagationGetterInterface::class); - $this->extendedPropagationGetter = Mockery::mock(ExtendedPropagationGetterInterface::class); + $inner = $this->createMock(PropagationGetterInterface::class); + $inner->method('get')->willReturn(',,key1=value1,key2=value2,,'); + + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); + + $this->assertSame('key1=value1,key2=value2', $getter->get([], 'tracestate')); } - public function test_get_all_from_carrier_with_semicolons(): void + public function test_keys_delegates_to_inner_getter(): void { - $carrier = ['a' => ['key1=value1;key2=value2', 'key3=value3']]; + $inner = $this->createMock(PropagationGetterInterface::class); + $inner->method('keys')->willReturn(['traceparent', 'tracestate']); - $this->extendedPropagationGetter->shouldReceive('getAll')->with($carrier, 'a')->andReturn(['key1=value1;key2=value2', 'key3=value3']); - $getter = new SanitizeCombinedHeadersPropagationGetter($this->extendedPropagationGetter); + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); - $this->assertSame(['key1=value1,key2=value2', 'key3=value3'], $getter->getAll($carrier, 'a')); + $this->assertSame(['traceparent', 'tracestate'], $getter->keys([])); } - public function test_get_all_from_carrier_with_leading_commas(): void + public function test_get_all_uses_get_all_when_inner_implements_extended_getter(): void { - $carrier = ['a' => [',,alpha,beta']]; + $inner = $this->createMock(ExtendedPropagationGetterInterface::class); + $inner->method('getAll')->willReturn(['key1=value1;key2=value2', 'key3=value3']); - $this->extendedPropagationGetter->shouldReceive('getAll')->with($carrier, 'a')->andReturn([',,alpha,beta']); - $getter = new SanitizeCombinedHeadersPropagationGetter($this->extendedPropagationGetter); + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); - $this->assertSame(['alpha,beta'], $getter->getAll($carrier, 'a')); + $this->assertSame(['key1=value1,key2=value2', 'key3=value3'], $getter->getAll([], 'tracestate')); } - public function test_get_all_from_not_existing_key(): void + public function test_get_all_falls_back_to_get_when_inner_does_not_implement_extended_getter(): void { - $carrier = ['a' => 'alpha']; + $inner = $this->createMock(PropagationGetterInterface::class); + $inner->method('get')->willReturn('key1=value1;key2=value2'); - $this->extendedPropagationGetter->shouldReceive('getAll')->with($carrier, 'b')->andReturn([]); - $getter = new SanitizeCombinedHeadersPropagationGetter($this->extendedPropagationGetter); + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); - $this->assertSame([], $getter->getAll($carrier, 'b')); + $this->assertSame(['key1=value1,key2=value2'], $getter->getAll([], 'tracestate')); } - public function test_get_all_from_carrier_without_implement_extended_getter(): void + public function test_get_all_returns_empty_array_when_value_is_empty(): void { - $carrier = ['a' => 'alpha']; + $inner = $this->createMock(ExtendedPropagationGetterInterface::class); + $inner->method('getAll')->willReturn([]); - $this->propagationGetter->shouldReceive('get')->with($carrier, 'a')->andReturn('alpha'); - $getter = new SanitizeCombinedHeadersPropagationGetter($this->propagationGetter); + $getter = new SanitizeCombinedHeadersPropagationGetter($inner); - $this->assertSame(['alpha'], $getter->getAll($carrier, 'a')); + $this->assertSame([], $getter->getAll([], 'tracestate')); } } diff --git a/tests/Unit/Contrib/Otlp/ContentTypesTest.php b/tests/Unit/Contrib/Otlp/ContentTypesTest.php new file mode 100644 index 000000000..d97a2be33 --- /dev/null +++ b/tests/Unit/Contrib/Otlp/ContentTypesTest.php @@ -0,0 +1,32 @@ +assertSame('application/x-protobuf', ContentTypes::PROTOBUF); + } + + public function test_json_constant(): void + { + $this->assertSame('application/json', ContentTypes::JSON); + } + + public function test_ndjson_constant(): void + { + $this->assertSame('application/x-ndjson', ContentTypes::NDJSON); + } +} diff --git a/tests/Unit/Contrib/Otlp/LogsExporterTest.php b/tests/Unit/Contrib/Otlp/LogsExporterTest.php index 425ecd6ee..b932a6193 100644 --- a/tests/Unit/Contrib/Otlp/LogsExporterTest.php +++ b/tests/Unit/Contrib/Otlp/LogsExporterTest.php @@ -52,6 +52,14 @@ public function test_export_success(): void $this->assertTrue($result->await()); } + public function test_export_success_with_null_payload(): void + { + $future = new CompletedFuture(null); + $this->transport->method('send')->willReturn($future); + $result = $this->exporter->export([]); + $this->assertTrue($result->await()); + } + public function test_shutdown(): void { $this->transport->expects($this->once())->method('shutdown'); diff --git a/tests/Unit/Contrib/Otlp/MetricExporterTest.php b/tests/Unit/Contrib/Otlp/MetricExporterTest.php index 63d7391d0..057f80286 100644 --- a/tests/Unit/Contrib/Otlp/MetricExporterTest.php +++ b/tests/Unit/Contrib/Otlp/MetricExporterTest.php @@ -33,6 +33,40 @@ public function setUp(): void $this->exporter = new MetricExporter($transport); } + public function test_temporality_returns_configured_temporality(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/x-ndjson'); + $exporter = new MetricExporter($transport, Temporality::DELTA); + $metric = $this->createMock(\OpenTelemetry\SDK\Metrics\MetricMetadataInterface::class); + $this->assertSame(Temporality::DELTA, $exporter->temporality($metric)); + } + + public function test_temporality_delegates_to_metric_when_null(): void + { + $metric = $this->createMock(\OpenTelemetry\SDK\Metrics\MetricMetadataInterface::class); + $metric->expects($this->once())->method('temporality')->willReturn(Temporality::CUMULATIVE); + $this->assertSame(Temporality::CUMULATIVE, $this->exporter->temporality($metric)); + } + + public function test_shutdown_delegates_to_transport(): void + { + $transport = $this->createMock(\OpenTelemetry\SDK\Common\Export\TransportInterface::class); + $transport->method('contentType')->willReturn('application/x-protobuf'); + $transport->expects($this->once())->method('shutdown')->willReturn(true); + $exporter = new MetricExporter($transport); + $this->assertTrue($exporter->shutdown()); + } + + public function test_force_flush_delegates_to_transport(): void + { + $transport = $this->createMock(\OpenTelemetry\SDK\Common\Export\TransportInterface::class); + $transport->method('contentType')->willReturn('application/x-protobuf'); + $transport->expects($this->once())->method('forceFlush')->willReturn(true); + $exporter = new MetricExporter($transport); + $this->assertTrue($exporter->forceFlush()); + } + public function test_exporter_writes_metrics_in_otlp_json_format_with_trailing_newline(): void { $this->exporter->export([ diff --git a/tests/Unit/Contrib/Otlp/OtlpUtilTest.php b/tests/Unit/Contrib/Otlp/OtlpUtilTest.php index 091b70c59..0c18075c9 100644 --- a/tests/Unit/Contrib/Otlp/OtlpUtilTest.php +++ b/tests/Unit/Contrib/Otlp/OtlpUtilTest.php @@ -136,4 +136,26 @@ public static function headersProvider(): array ], ]; } + + #[DataProvider('pathProvider')] + public function test_path(string $signal, string $protocol, string $expected): void + { + $path = OtlpUtil::path($signal, $protocol); + $this->assertSame($expected, $path); + } + + public static function pathProvider(): array + { + return [ + 'grpc trace' => [Signals::TRACE, 'grpc', '/opentelemetry.proto.collector.trace.v1.TraceService/Export'], + 'grpc metrics' => [Signals::METRICS, 'grpc', '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export'], + 'grpc logs' => [Signals::LOGS, 'grpc', '/opentelemetry.proto.collector.logs.v1.LogsService/Export'], + 'http/protobuf trace' => [Signals::TRACE, 'http/protobuf', '/v1/traces'], + 'http/protobuf metrics' => [Signals::METRICS, 'http/protobuf', '/v1/metrics'], + 'http/protobuf logs' => [Signals::LOGS, 'http/protobuf', '/v1/logs'], + 'http/json trace' => [Signals::TRACE, 'http/json', '/v1/traces'], + 'http/json metrics' => [Signals::METRICS, 'http/json', '/v1/metrics'], + 'http/json logs' => [Signals::LOGS, 'http/json', '/v1/logs'], + ]; + } } diff --git a/tests/Unit/Contrib/Otlp/ProtocolsTest.php b/tests/Unit/Contrib/Otlp/ProtocolsTest.php new file mode 100644 index 000000000..3f309e41d --- /dev/null +++ b/tests/Unit/Contrib/Otlp/ProtocolsTest.php @@ -0,0 +1,50 @@ +assertSame($expected, Protocols::contentType($protocol)); + } + + public static function protocolProvider(): array + { + return [ + 'grpc' => [Protocols::GRPC, ContentTypes::PROTOBUF], + 'http/protobuf' => [Protocols::HTTP_PROTOBUF, ContentTypes::PROTOBUF], + 'http/json' => [Protocols::HTTP_JSON, ContentTypes::JSON], + 'http/ndjson' => [Protocols::HTTP_NDJSON, ContentTypes::NDJSON], + ]; + } + + public function test_validate_throws_for_unknown_protocol(): void + { + $this->expectException(UnexpectedValueException::class); + Protocols::validate('unknown'); + } + + public function test_validate_succeeds_for_known_protocol(): void + { + Protocols::validate(Protocols::GRPC); + $this->assertTrue(true); + } + + public function test_content_type_throws_for_unknown_protocol(): void + { + $this->expectException(UnexpectedValueException::class); + Protocols::contentType('unknown'); + } +} diff --git a/tests/Unit/Contrib/Otlp/SpanExporterTest.php b/tests/Unit/Contrib/Otlp/SpanExporterTest.php index 473f17b61..d5a560258 100644 --- a/tests/Unit/Contrib/Otlp/SpanExporterTest.php +++ b/tests/Unit/Contrib/Otlp/SpanExporterTest.php @@ -57,6 +57,14 @@ public function test_export_success(): void $this->assertTrue($result->await()); } + public function test_export_success_with_null_payload(): void + { + $future = new CompletedFuture(null); + $this->transport->method('send')->willReturn($future); + $result = $this->exporter->export([]); + $this->assertTrue($result->await()); + } + public function test_shutdown(): void { $this->transport->expects($this->once())->method('shutdown'); diff --git a/tests/Unit/SDK/Common/Configuration/EnvComponentLoaderRegistryTest.php b/tests/Unit/SDK/Common/Configuration/EnvComponentLoaderRegistryTest.php new file mode 100644 index 000000000..bbaa4d167 --- /dev/null +++ b/tests/Unit/SDK/Common/Configuration/EnvComponentLoaderRegistryTest.php @@ -0,0 +1,144 @@ +register($loader); + + $env = $this->createMock(EnvResolver::class); + $result = $registry->load('string', 'test_loader', $env, new Context()); + + $this->assertSame('loaded-value', $result); + } + + public function test_register_returns_self(): void + { + $registry = new EnvComponentLoaderRegistry(); + + $loader = new class implements EnvComponentLoader { + public function load(EnvResolver $env, EnvComponentLoaderRegistryInterface $registry, Context $context): string + { + return 'value'; + } + + public function name(): string + { + return 'fluent_loader'; + } + }; + + $result = $registry->register($loader); + $this->assertSame($registry, $result); + } + + public function test_register_duplicate_throws(): void + { + $registry = new EnvComponentLoaderRegistry(); + + $loader = new class implements EnvComponentLoader { + public function load(EnvResolver $env, EnvComponentLoaderRegistryInterface $registry, Context $context): string + { + return 'value'; + } + + public function name(): string + { + return 'dup_loader'; + } + }; + + $registry->register($loader); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Duplicate environment loader'); + $registry->register($loader); + } + + public function test_load_unknown_throws(): void + { + $registry = new EnvComponentLoaderRegistry(); + $env = $this->createMock(EnvResolver::class); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Loader for string unknown not found'); + $registry->load('string', 'unknown', $env, new Context()); + } + + public function test_load_all(): void + { + $registry = new EnvComponentLoaderRegistry(); + + $loader1 = new class implements EnvComponentLoader { + public function load(EnvResolver $env, EnvComponentLoaderRegistryInterface $registry, Context $context): string + { + return 'value1'; + } + + public function name(): string + { + return 'loader1'; + } + }; + + $loader2 = new class implements EnvComponentLoader { + public function load(EnvResolver $env, EnvComponentLoaderRegistryInterface $registry, Context $context): string + { + return 'value2'; + } + + public function name(): string + { + return 'loader2'; + } + }; + + $registry->register($loader1); + $registry->register($loader2); + + $env = $this->createMock(EnvResolver::class); + $results = iterator_to_array($registry->loadAll('string', $env, new Context())); + + $this->assertCount(2, $results); + $this->assertSame('value1', $results[0]); + $this->assertSame('value2', $results[1]); + } + + public function test_load_all_empty_type(): void + { + $registry = new EnvComponentLoaderRegistry(); + $env = $this->createMock(EnvResolver::class); + + $results = iterator_to_array($registry->loadAll('nonexistent', $env, new Context())); + $this->assertEmpty($results); + } +} diff --git a/tests/Unit/SDK/Common/Configuration/EnvResolverTest.php b/tests/Unit/SDK/Common/Configuration/EnvResolverTest.php new file mode 100644 index 000000000..91de3b0e6 --- /dev/null +++ b/tests/Unit/SDK/Common/Configuration/EnvResolverTest.php @@ -0,0 +1,121 @@ +resolver = new EnvResolver(); + } + + public function test_string_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->string('NONEXISTENT_VAR_12345')); + } + + public function test_string_returns_value(): void + { + $this->setEnvironmentVariable('TEST_STRING_VAR', 'hello'); + $this->assertSame('hello', $this->resolver->string('TEST_STRING_VAR')); + } + + public function test_bool_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->bool('NONEXISTENT_VAR_12345')); + } + + public function test_bool_returns_value(): void + { + $this->setEnvironmentVariable('TEST_BOOL_VAR', 'true'); + $this->assertTrue($this->resolver->bool('TEST_BOOL_VAR')); + } + + public function test_int_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->int('NONEXISTENT_VAR_12345')); + } + + public function test_int_returns_value(): void + { + $this->setEnvironmentVariable('TEST_INT_VAR', '42'); + $this->assertSame(42, $this->resolver->int('TEST_INT_VAR')); + } + + public function test_int_returns_null_when_out_of_range(): void + { + $this->setEnvironmentVariable('TEST_INT_VAR', '100'); + $this->assertNull($this->resolver->int('TEST_INT_VAR', 0, 50)); + } + + public function test_numeric_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->numeric('NONEXISTENT_VAR_12345')); + } + + public function test_numeric_returns_value(): void + { + $this->setEnvironmentVariable('TEST_NUMERIC_VAR', '3.14'); + $this->assertSame(3.14, $this->resolver->numeric('TEST_NUMERIC_VAR')); + } + + public function test_numeric_returns_null_when_out_of_range(): void + { + $this->setEnvironmentVariable('TEST_NUMERIC_VAR', '100.5'); + $this->assertNull($this->resolver->numeric('TEST_NUMERIC_VAR', 0, 50)); + } + + public function test_list_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->list('NONEXISTENT_VAR_12345')); + } + + public function test_list_returns_value(): void + { + $this->setEnvironmentVariable('TEST_LIST_VAR', 'a,b,c'); + $this->assertSame(['a', 'b', 'c'], $this->resolver->list('TEST_LIST_VAR')); + } + + public function test_map_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->map('NONEXISTENT_VAR_12345')); + } + + public function test_map_returns_value(): void + { + $this->setEnvironmentVariable('TEST_MAP_VAR', 'key1=val1,key2=val2'); + $result = $this->resolver->map('TEST_MAP_VAR'); + $this->assertSame('val1', $result['key1']); + $this->assertSame('val2', $result['key2']); + } + + public function test_enum_returns_null_when_not_set(): void + { + $this->assertNull($this->resolver->enum('NONEXISTENT_VAR_12345', ['a', 'b'])); + } + + public function test_enum_returns_value_when_valid(): void + { + $this->setEnvironmentVariable('TEST_ENUM_VAR', 'a'); + $this->assertSame('a', $this->resolver->enum('TEST_ENUM_VAR', ['a', 'b'])); + } + + public function test_enum_returns_null_when_invalid(): void + { + $this->setEnvironmentVariable('TEST_ENUM_VAR', 'c'); + $this->assertNull($this->resolver->enum('TEST_ENUM_VAR', ['a', 'b'])); + } +} diff --git a/tests/Unit/SDK/Common/Exception/StackTraceFormatterTest.php b/tests/Unit/SDK/Common/Exception/StackTraceFormatterTest.php new file mode 100644 index 000000000..94844322d --- /dev/null +++ b/tests/Unit/SDK/Common/Exception/StackTraceFormatterTest.php @@ -0,0 +1,78 @@ +assertStringContainsString('Exception', $result); + $this->assertStringContainsString('test error', $result); + $this->assertStringContainsString('StackTraceFormatterTest.php', $result); + } + + public function test_format_exception_without_message(): void + { + $e = new Exception(); + $result = StackTraceFormatter::format($e); + $this->assertStringContainsString('Exception', $result); + $this->assertStringNotContainsString(': ', explode("\n", $result)[0]); + } + + public function test_format_chained_exception(): void + { + $cause = new RuntimeException('root cause'); + $e = new Exception('wrapper', 0, $cause); + $result = StackTraceFormatter::format($e); + $this->assertStringContainsString('Caused by:', $result); + $this->assertStringContainsString('root cause', $result); + $this->assertStringContainsString('RuntimeException', $result); + } + + public function test_format_deeply_chained_exception(): void + { + $e1 = new RuntimeException('first'); + $e2 = new Exception('second', 0, $e1); + $e3 = new Exception('third', 0, $e2); + $result = StackTraceFormatter::format($e3); + $this->assertSame(2, substr_count($result, 'Caused by:')); + $this->assertStringContainsString('first', $result); + $this->assertStringContainsString('second', $result); + $this->assertStringContainsString('third', $result); + } + + public function test_format_contains_at_prefix_for_frames(): void + { + $e = new Exception('test'); + $result = StackTraceFormatter::format($e); + $this->assertStringContainsString("\tat ", $result); + } + + public function test_format_common_frames_shows_more(): void + { + $cause = new RuntimeException('inner'); + $e = new Exception('outer', 0, $cause); + $result = StackTraceFormatter::format($e); + $this->assertStringContainsString('... ', $result); + $this->assertStringContainsString(' more', $result); + } + + public function test_format_uses_dot_notation_for_namespace(): void + { + $e = new Exception('test'); + $result = StackTraceFormatter::format($e); + // PHP namespaces use backslashes, but formatter converts to dots + $this->assertStringNotContainsString('\\', $result); + } +} diff --git a/tests/Unit/SDK/Common/Export/Stream/StreamTransportTest.php b/tests/Unit/SDK/Common/Export/Stream/StreamTransportTest.php new file mode 100644 index 000000000..874b0f968 --- /dev/null +++ b/tests/Unit/SDK/Common/Export/Stream/StreamTransportTest.php @@ -0,0 +1,70 @@ +assertSame('application/json', $transport->contentType()); + } + + public function test_send_writes_payload(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/json'); + $future = $transport->send('test payload'); + $this->assertNull($future->await()); + fseek($stream, 0); + $this->assertSame('test payload', stream_get_contents($stream)); + } + + public function test_send_after_shutdown_returns_error(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/json'); + $transport->shutdown(); + $future = $transport->send('test'); + $this->expectException(\BadMethodCallException::class); + $future->await(); + } + + public function test_shutdown_returns_true(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/json'); + $this->assertTrue($transport->shutdown()); + } + + public function test_shutdown_twice_returns_false(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/json'); + $transport->shutdown(); + $this->assertFalse($transport->shutdown()); + } + + public function test_force_flush_returns_true(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/json'); + $this->assertTrue($transport->forceFlush()); + } + + public function test_force_flush_after_shutdown_returns_false(): void + { + $stream = fopen('php://memory', 'a+b'); + $transport = new StreamTransport($stream, 'application/json'); + $transport->shutdown(); + $this->assertFalse($transport->forceFlush()); + } +} diff --git a/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorClosureTest.php b/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorClosureTest.php new file mode 100644 index 000000000..d02586b90 --- /dev/null +++ b/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorClosureTest.php @@ -0,0 +1,104 @@ + null; + $configurator = new ConfiguratorClosure($closure, null, null, null); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('anything'); + $scope->method('getVersion')->willReturn('1.0'); + $scope->method('getSchemaUrl')->willReturn('https://example.com'); + + $this->assertTrue($configurator->matches($scope)); + } + + public function test_matches_returns_true_when_name_regex_matches(): void + { + $closure = static fn () => null; + $configurator = new ConfiguratorClosure($closure, '/^my-lib/', null, null); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('my-lib-component'); + + $this->assertTrue($configurator->matches($scope)); + } + + public function test_matches_returns_false_when_name_regex_does_not_match(): void + { + $closure = static fn () => null; + $configurator = new ConfiguratorClosure($closure, '/^my-lib/', null, null); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('other-lib'); + + $this->assertFalse($configurator->matches($scope)); + } + + public function test_matches_returns_true_when_version_matches(): void + { + $closure = static fn () => null; + $configurator = new ConfiguratorClosure($closure, null, '2.0', null); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getVersion')->willReturn('2.0'); + + $this->assertTrue($configurator->matches($scope)); + } + + public function test_matches_returns_false_when_version_does_not_match(): void + { + $closure = static fn () => null; + $configurator = new ConfiguratorClosure($closure, null, '2.0', null); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('x'); + $scope->method('getVersion')->willReturn('1.0'); + + $this->assertFalse($configurator->matches($scope)); + } + + public function test_matches_returns_true_when_schema_url_matches(): void + { + $closure = static fn () => null; + $configurator = new ConfiguratorClosure($closure, null, null, 'https://example.com/schema'); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('x'); + $scope->method('getSchemaUrl')->willReturn('https://example.com/schema'); + + $this->assertTrue($configurator->matches($scope)); + } + + public function test_matches_returns_false_when_schema_url_does_not_match(): void + { + $closure = static fn () => null; + $configurator = new ConfiguratorClosure($closure, null, null, 'https://example.com/schema'); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('x'); + $scope->method('getSchemaUrl')->willReturn('https://other.com/schema'); + + $this->assertFalse($configurator->matches($scope)); + } + + public function test_closure_property_is_accessible(): void + { + $closure = static fn () => 'result'; + $configurator = new ConfiguratorClosure($closure, null, null, null); + + $this->assertSame($closure, $configurator->closure); + } +} diff --git a/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorTest.php b/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorTest.php index b986451aa..8745f8489 100644 --- a/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorTest.php +++ b/tests/Unit/SDK/Common/InstrumentationScope/ConfiguratorTest.php @@ -49,4 +49,55 @@ public function test_returns_default_on_no_match(): void $this->assertTrue($config->isEnabled()); } + + public function test_match_wildcard_name(): void + { + $configurator = $this->configurator->with(static fn (Config $config) => $config->setDisabled(true), name: 'te*'); + $this->assertFalse($configurator->resolve($this->scope)->isEnabled()); + } + + public function test_no_match_wildcard_name(): void + { + $configurator = $this->configurator->with(static fn (Config $config) => $config->setDisabled(true), name: 'other*'); + $this->assertTrue($configurator->resolve($this->scope)->isEnabled()); + } + + public function test_match_null_name(): void + { + $configurator = $this->configurator->with(static fn (Config $config) => $config->setDisabled(true), name: null); + $this->assertFalse($configurator->resolve($this->scope)->isEnabled()); + } + + public function test_resolve_caches_config(): void + { + $config1 = $this->configurator->resolve($this->scope); + $config2 = $this->configurator->resolve($this->scope); + $this->assertSame($config1, $config2); + } + + public function test_with_applies_to_already_resolved_config(): void + { + $config = $this->configurator->resolve($this->scope); + $this->assertTrue($config->isEnabled()); + $this->configurator->with(static fn (Config $config) => $config->setDisabled(true), name: 'test'); + $this->assertFalse($config->isEnabled()); + } + + public function test_logger_factory(): void + { + $configurator = Configurator::logger(); + $this->assertInstanceOf(Configurator::class, $configurator); + } + + public function test_meter_factory(): void + { + $configurator = Configurator::meter(); + $this->assertInstanceOf(Configurator::class, $configurator); + } + + public function test_tracer_factory(): void + { + $configurator = Configurator::tracer(); + $this->assertInstanceOf(Configurator::class, $configurator); + } } diff --git a/tests/Unit/SDK/Common/Util/FunctionsTest.php b/tests/Unit/SDK/Common/Util/FunctionsTest.php new file mode 100644 index 000000000..8a1b8387f --- /dev/null +++ b/tests/Unit/SDK/Common/Util/FunctionsTest.php @@ -0,0 +1,100 @@ +assertInstanceOf(Closure::class, $result); + $this->assertSame(5, $result('hello')); + } + + public function test_closure_from_invokable_object(): void + { + $invokable = new class() { + public function __invoke(int $x): int + { + return $x * 2; + } + }; + + $result = closure($invokable); + + $this->assertInstanceOf(Closure::class, $result); + $this->assertSame(10, $result(5)); + } + + public function test_closure_from_array_callable(): void + { + $object = new class() { + public function add(int $a, int $b): int + { + return $a + $b; + } + }; + + $result = closure([$object, 'add']); + + $this->assertInstanceOf(Closure::class, $result); + $this->assertSame(7, $result(3, 4)); + } + + public function test_weaken_returns_closure_without_bound_this(): void + { + $fn = static fn (int $x): int => $x + 1; + + $weakened = weaken($fn); + + $this->assertSame(6, $weakened(5)); + } + + public function test_weaken_weakens_bound_object(): void + { + $object = new class() { + public int $value = 42; + + public function getValue(): int + { + return $this->value; + } + }; + + $target = null; + $weakened = weaken(closure($object->getValue(...)), $target); + + $this->assertSame($object, $target); + $this->assertSame(42, $weakened()); + } + + public function test_weaken_returns_null_after_target_is_collected(): void + { + $object = new class() { + public function getValue(): int + { + return 42; + } + }; + + $target = null; + $weakened = weaken(closure($object->getValue(...)), $target); + + // Remove all strong references + unset($object, $target); + + $this->assertNull($weakened()); + } +} diff --git a/tests/Unit/SDK/Logs/LogRecordBuilderTest.php b/tests/Unit/SDK/Logs/LogRecordBuilderTest.php new file mode 100644 index 000000000..203ef4bbc --- /dev/null +++ b/tests/Unit/SDK/Logs/LogRecordBuilderTest.php @@ -0,0 +1,164 @@ +logger = $this->createMock(LoggerInterface::class); + $this->builder = new LogRecordBuilder($this->logger); + } + + public function test_set_timestamp_returns_self(): void + { + $result = $this->builder->setTimestamp(1234567890); + + $this->assertSame($this->builder, $result); + } + + public function test_set_observed_timestamp_returns_self(): void + { + $result = $this->builder->setObservedTimestamp(1234567890); + + $this->assertSame($this->builder, $result); + } + + public function test_set_context_returns_self(): void + { + $context = $this->createMock(\OpenTelemetry\Context\ContextInterface::class); + $result = $this->builder->setContext($context); + + $this->assertSame($this->builder, $result); + } + + public function test_set_context_with_false_uses_root_context(): void + { + $result = $this->builder->setContext(false); + + $this->assertSame($this->builder, $result); + } + + public function test_set_context_with_null_returns_self(): void + { + $result = $this->builder->setContext(null); + + $this->assertSame($this->builder, $result); + } + + public function test_set_severity_number_with_int_returns_self(): void + { + $result = $this->builder->setSeverityNumber(9); + + $this->assertSame($this->builder, $result); + } + + public function test_set_severity_number_with_severity_enum_returns_self(): void + { + $result = $this->builder->setSeverityNumber(Severity::INFO); + + $this->assertSame($this->builder, $result); + } + + public function test_set_severity_text_returns_self(): void + { + $result = $this->builder->setSeverityText('INFO'); + + $this->assertSame($this->builder, $result); + } + + public function test_set_body_returns_self(): void + { + $result = $this->builder->setBody('log message body'); + + $this->assertSame($this->builder, $result); + } + + public function test_set_attribute_returns_self(): void + { + $result = $this->builder->setAttribute('key', 'value'); + + $this->assertSame($this->builder, $result); + } + + public function test_set_attributes_returns_self(): void + { + $result = $this->builder->setAttributes(['key1' => 'value1', 'key2' => 'value2']); + + $this->assertSame($this->builder, $result); + } + + public function test_set_exception_returns_self(): void + { + $exception = new \RuntimeException('test error'); + $result = $this->builder->setException($exception); + + $this->assertSame($this->builder, $result); + } + + public function test_set_exception_sets_exception_attributes(): void + { + $exception = new \RuntimeException('test error'); + + $this->logger->expects($this->once()) + ->method('emit') + ->with($this->callback(function (LogRecord $logRecord) { + $reflection = new \ReflectionProperty($logRecord, 'attributes'); + $attributes = $reflection->getValue($logRecord); + + return $attributes['exception.message'] === 'test error' + && $attributes['exception.type'] === \RuntimeException::class + && isset($attributes['exception.stacktrace']); + })); + + $this->builder->setException($exception); + $this->builder->emit(); + } + + public function test_set_event_name_returns_self(): void + { + $result = $this->builder->setEventName('my.event'); + + $this->assertSame($this->builder, $result); + } + + public function test_emit_calls_logger_emit(): void + { + $this->logger->expects($this->once()) + ->method('emit') + ->with($this->isInstanceOf(LogRecord::class)); + + $this->builder->emit(); + } + + public function test_fluent_interface(): void + { + $this->logger->expects($this->once()) + ->method('emit') + ->with($this->isInstanceOf(LogRecord::class)); + + $this->builder + ->setTimestamp(1234567890) + ->setObservedTimestamp(1234567891) + ->setSeverityNumber(Severity::ERROR) + ->setSeverityText('ERROR') + ->setBody('something went wrong') + ->setAttribute('key', 'value') + ->setAttributes(['key2' => 'value2']) + ->setEventName('my.event') + ->emit(); + } +} diff --git a/tests/Unit/SDK/Logs/LogRecordLimitsTest.php b/tests/Unit/SDK/Logs/LogRecordLimitsTest.php new file mode 100644 index 000000000..cd4a3e122 --- /dev/null +++ b/tests/Unit/SDK/Logs/LogRecordLimitsTest.php @@ -0,0 +1,22 @@ +createMock(AttributesFactoryInterface::class); + $limits = new LogRecordLimits($factory); + + $this->assertSame($factory, $limits->getAttributeFactory()); + } +} diff --git a/tests/Unit/SDK/Logs/LoggerConfigTest.php b/tests/Unit/SDK/Logs/LoggerConfigTest.php new file mode 100644 index 000000000..a1a3c6753 --- /dev/null +++ b/tests/Unit/SDK/Logs/LoggerConfigTest.php @@ -0,0 +1,46 @@ +assertInstanceOf(LoggerConfig::class, $config); + $this->assertInstanceOf(Config::class, $config); + } + + public function test_is_enabled_by_default(): void + { + $config = LoggerConfig::default(); + + $this->assertTrue($config->isEnabled()); + } + + public function test_set_disabled(): void + { + $config = LoggerConfig::default(); + $config->setDisabled(true); + + $this->assertFalse($config->isEnabled()); + } + + public function test_set_disabled_false_re_enables(): void + { + $config = LoggerConfig::default(); + $config->setDisabled(true); + $config->setDisabled(false); + + $this->assertTrue($config->isEnabled()); + } +} diff --git a/tests/Unit/SDK/Metrics/Data/ExemplarTest.php b/tests/Unit/SDK/Metrics/Data/ExemplarTest.php new file mode 100644 index 000000000..c27c48dc7 --- /dev/null +++ b/tests/Unit/SDK/Metrics/Data/ExemplarTest.php @@ -0,0 +1,92 @@ +createMock(AttributesInterface::class); + + $exemplar = new Exemplar( + index: 0, + value: 42.5, + timestamp: 1234567890, + attributes: $attributes, + traceId: 'abc123', + spanId: 'def456', + ); + + $this->assertSame(42.5, $exemplar->value); + $this->assertSame(1234567890, $exemplar->timestamp); + $this->assertSame($attributes, $exemplar->attributes); + $this->assertSame('abc123', $exemplar->traceId); + $this->assertSame('def456', $exemplar->spanId); + } + + public function test_constructor_with_nullable_trace_and_span(): void + { + $attributes = $this->createMock(AttributesInterface::class); + + $exemplar = new Exemplar( + index: 1, + value: 10, + timestamp: 100, + attributes: $attributes, + traceId: null, + spanId: null, + ); + + $this->assertNull($exemplar->traceId); + $this->assertNull($exemplar->spanId); + } + + public function test_constructor_with_string_index(): void + { + $attributes = $this->createMock(AttributesInterface::class); + + $exemplar = new Exemplar( + index: 'key', + value: 99, + timestamp: 200, + attributes: $attributes, + traceId: null, + spanId: null, + ); + + $this->assertSame(99, $exemplar->value); + } + + public function test_group_by_index(): void + { + $attributes = $this->createMock(AttributesInterface::class); + + $e1 = new Exemplar(0, 1.0, 100, $attributes, null, null); + $e2 = new Exemplar(1, 2.0, 200, $attributes, null, null); + $e3 = new Exemplar(0, 3.0, 300, $attributes, null, null); + + $grouped = Exemplar::groupByIndex([$e1, $e2, $e3]); + + $this->assertCount(2, $grouped); + $this->assertCount(2, $grouped[0]); + $this->assertCount(1, $grouped[1]); + $this->assertSame($e1, $grouped[0][0]); + $this->assertSame($e3, $grouped[0][1]); + $this->assertSame($e2, $grouped[1][0]); + } + + public function test_group_by_index_with_empty_iterable(): void + { + $grouped = Exemplar::groupByIndex([]); + + $this->assertSame([], $grouped); + } +} diff --git a/tests/Unit/SDK/Metrics/Data/HistogramDataPointTest.php b/tests/Unit/SDK/Metrics/Data/HistogramDataPointTest.php new file mode 100644 index 000000000..2be61c23a --- /dev/null +++ b/tests/Unit/SDK/Metrics/Data/HistogramDataPointTest.php @@ -0,0 +1,83 @@ +createMock(AttributesInterface::class); + + $dataPoint = new HistogramDataPoint( + count: 10, + sum: 55.5, + min: 1.0, + max: 10.0, + bucketCounts: [2, 3, 5], + explicitBounds: [5.0, 10.0], + attributes: $attributes, + startTimestamp: 1000, + timestamp: 2000, + exemplars: [], + ); + + $this->assertSame(10, $dataPoint->count); + $this->assertSame(55.5, $dataPoint->sum); + $this->assertSame(1.0, $dataPoint->min); + $this->assertSame(10.0, $dataPoint->max); + $this->assertSame([2, 3, 5], $dataPoint->bucketCounts); + $this->assertSame([5.0, 10.0], $dataPoint->explicitBounds); + $this->assertSame($attributes, $dataPoint->attributes); + $this->assertSame(1000, $dataPoint->startTimestamp); + $this->assertSame(2000, $dataPoint->timestamp); + $this->assertSame([], $dataPoint->exemplars); + } + + public function test_constructor_with_integer_values(): void + { + $attributes = $this->createMock(AttributesInterface::class); + + $dataPoint = new HistogramDataPoint( + count: 5, + sum: 100, + min: 10, + max: 30, + bucketCounts: [1, 2, 2], + explicitBounds: [15, 25], + attributes: $attributes, + startTimestamp: 500, + timestamp: 1000, + ); + + $this->assertSame(100, $dataPoint->sum); + $this->assertSame(10, $dataPoint->min); + $this->assertSame(30, $dataPoint->max); + } + + public function test_exemplars_defaults_to_empty(): void + { + $attributes = $this->createMock(AttributesInterface::class); + + $dataPoint = new HistogramDataPoint( + count: 0, + sum: 0, + min: 0, + max: 0, + bucketCounts: [], + explicitBounds: [], + attributes: $attributes, + startTimestamp: 0, + timestamp: 0, + ); + + $this->assertSame([], $dataPoint->exemplars); + } +} diff --git a/tests/Unit/SDK/Metrics/InstrumentTypeTest.php b/tests/Unit/SDK/Metrics/InstrumentTypeTest.php new file mode 100644 index 000000000..f4d167783 --- /dev/null +++ b/tests/Unit/SDK/Metrics/InstrumentTypeTest.php @@ -0,0 +1,55 @@ +assertSame('Counter', InstrumentType::COUNTER); + } + + public function test_up_down_counter_constant(): void + { + $this->assertSame('UpDownCounter', InstrumentType::UP_DOWN_COUNTER); + } + + public function test_histogram_constant(): void + { + $this->assertSame('Histogram', InstrumentType::HISTOGRAM); + } + + public function test_gauge_constant(): void + { + $this->assertSame('Gauge', InstrumentType::GAUGE); + } + + public function test_asynchronous_counter_constant(): void + { + $this->assertSame('AsynchronousCounter', InstrumentType::ASYNCHRONOUS_COUNTER); + } + + public function test_asynchronous_up_down_counter_constant(): void + { + $this->assertSame('AsynchronousUpDownCounter', InstrumentType::ASYNCHRONOUS_UP_DOWN_COUNTER); + } + + public function test_asynchronous_gauge_constant(): void + { + $this->assertSame('AsynchronousGauge', InstrumentType::ASYNCHRONOUS_GAUGE); + } + + public function test_class_is_not_instantiable(): void + { + $reflection = new ReflectionClass(InstrumentType::class); + $this->assertFalse($reflection->isInstantiable()); + } +} diff --git a/tests/Unit/SDK/Metrics/MeterConfigTest.php b/tests/Unit/SDK/Metrics/MeterConfigTest.php new file mode 100644 index 000000000..ff439c826 --- /dev/null +++ b/tests/Unit/SDK/Metrics/MeterConfigTest.php @@ -0,0 +1,46 @@ +assertInstanceOf(MeterConfig::class, $config); + $this->assertInstanceOf(Config::class, $config); + } + + public function test_is_enabled_by_default(): void + { + $config = MeterConfig::default(); + + $this->assertTrue($config->isEnabled()); + } + + public function test_set_disabled(): void + { + $config = MeterConfig::default(); + $config->setDisabled(true); + + $this->assertFalse($config->isEnabled()); + } + + public function test_set_disabled_false_re_enables(): void + { + $config = MeterConfig::default(); + $config->setDisabled(true); + $config->setDisabled(false); + + $this->assertTrue($config->isEnabled()); + } +} diff --git a/tests/Unit/SDK/Metrics/MeterProviderBuilderTest.php b/tests/Unit/SDK/Metrics/MeterProviderBuilderTest.php new file mode 100644 index 000000000..b8a632c0c --- /dev/null +++ b/tests/Unit/SDK/Metrics/MeterProviderBuilderTest.php @@ -0,0 +1,66 @@ +build(); + $this->assertInstanceOf(MeterProviderInterface::class, $provider); + } + + public function test_set_resource(): void + { + $builder = new MeterProviderBuilder(); + $result = $builder->setResource($this->createMock(ResourceInfo::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(MeterProviderInterface::class, $builder->build()); + } + + public function test_set_exemplar_filter(): void + { + $builder = new MeterProviderBuilder(); + $result = $builder->setExemplarFilter($this->createMock(ExemplarFilterInterface::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(MeterProviderInterface::class, $builder->build()); + } + + public function test_add_reader(): void + { + $builder = new MeterProviderBuilder(); + $result = $builder->addReader($this->createMock(MetricReaderInterface::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(MeterProviderInterface::class, $builder->build()); + } + + public function test_set_configurator(): void + { + $builder = new MeterProviderBuilder(); + $result = $builder->setConfigurator(Configurator::meter()); + $this->assertSame($builder, $result); + $this->assertInstanceOf(MeterProviderInterface::class, $builder->build()); + } + + public function test_set_clock(): void + { + $builder = new MeterProviderBuilder(); + $result = $builder->setClock($this->createMock(ClockInterface::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(MeterProviderInterface::class, $builder->build()); + } +} diff --git a/tests/Unit/SDK/Metrics/MetricExporter/ConsoleMetricExporterTest.php b/tests/Unit/SDK/Metrics/MetricExporter/ConsoleMetricExporterTest.php new file mode 100644 index 000000000..4973ddd12 --- /dev/null +++ b/tests/Unit/SDK/Metrics/MetricExporter/ConsoleMetricExporterTest.php @@ -0,0 +1,145 @@ +createMetric(); + + ob_start(); + $result = $exporter->export([$metric]); + ob_end_clean(); + + $this->assertTrue($result); + } + + public function test_export_outputs_json(): void + { + $exporter = new ConsoleMetricExporter(); + + $metric = $this->createMetric('test_metric', 'ms', 'A test metric'); + + ob_start(); + $exporter->export([$metric]); + $output = ob_get_clean(); + + $decoded = json_decode($output, true); + $this->assertIsArray($decoded); + $this->assertArrayHasKey('resource', $decoded); + $this->assertArrayHasKey('scope', $decoded); + $this->assertArrayHasKey('metrics', $decoded['scope']); + $this->assertCount(1, $decoded['scope']['metrics']); + $this->assertSame('test_metric', $decoded['scope']['metrics'][0]['name']); + $this->assertSame('ms', $decoded['scope']['metrics'][0]['unit']); + $this->assertSame('A test metric', $decoded['scope']['metrics'][0]['description']); + } + + public function test_export_with_multiple_metrics(): void + { + $exporter = new ConsoleMetricExporter(); + + $metric1 = $this->createMetric('metric_one'); + $metric2 = $this->createMetric('metric_two'); + + ob_start(); + $exporter->export([$metric1, $metric2]); + $output = ob_get_clean(); + + $decoded = json_decode($output, true); + $this->assertCount(2, $decoded['scope']['metrics']); + $this->assertSame('metric_one', $decoded['scope']['metrics'][0]['name']); + $this->assertSame('metric_two', $decoded['scope']['metrics'][1]['name']); + } + + public function test_export_with_empty_batch(): void + { + $exporter = new ConsoleMetricExporter(); + + ob_start(); + $result = $exporter->export([]); + $output = ob_get_clean(); + + $this->assertTrue($result); + $decoded = json_decode($output, true); + $this->assertIsArray($decoded); + $this->assertNull($decoded['resource']); + $this->assertNull($decoded['scope']); + } + + public function test_shutdown_returns_true(): void + { + $exporter = new ConsoleMetricExporter(); + $this->assertTrue($exporter->shutdown()); + } + + public function test_force_flush_returns_true(): void + { + $exporter = new ConsoleMetricExporter(); + $this->assertTrue($exporter->forceFlush()); + } + + public function test_temporality_returns_configured_temporality(): void + { + $exporter = new ConsoleMetricExporter(Temporality::DELTA); + + $metric = $this->createMock(MetricMetadataInterface::class); + $metric->method('temporality')->willReturn(Temporality::CUMULATIVE); + + $this->assertSame(Temporality::DELTA, $exporter->temporality($metric)); + } + + public function test_temporality_returns_metric_temporality_when_not_configured(): void + { + $exporter = new ConsoleMetricExporter(); + + $metric = $this->createMock(MetricMetadataInterface::class); + $metric->method('temporality')->willReturn(Temporality::CUMULATIVE); + + $this->assertSame(Temporality::CUMULATIVE, $exporter->temporality($metric)); + } + + private function createMetric( + string $name = 'test', + ?string $unit = null, + ?string $description = null, + ): Metric { + $attributes = $this->createMock(AttributesInterface::class); + $attributes->method('toArray')->willReturn(['service.name' => 'test-service']); + $attributes->method('getDroppedAttributesCount')->willReturn(0); + + $resource = $this->createMock(ResourceInfo::class); + $resource->method('getAttributes')->willReturn($attributes); + + $scopeAttributes = $this->createMock(AttributesInterface::class); + $scopeAttributes->method('toArray')->willReturn([]); + $scopeAttributes->method('getDroppedAttributesCount')->willReturn(0); + + $scope = $this->createMock(InstrumentationScopeInterface::class); + $scope->method('getName')->willReturn('test-scope'); + $scope->method('getVersion')->willReturn('1.0.0'); + $scope->method('getAttributes')->willReturn($scopeAttributes); + $scope->method('getSchemaUrl')->willReturn(null); + + $data = $this->createMock(DataInterface::class); + + return new Metric($scope, $resource, $name, $unit, $description, $data); + } +} diff --git a/tests/Unit/SDK/Metrics/MetricExporter/InMemoryExporterTest.php b/tests/Unit/SDK/Metrics/MetricExporter/InMemoryExporterTest.php index fa1b5f4aa..97a6fc657 100644 --- a/tests/Unit/SDK/Metrics/MetricExporter/InMemoryExporterTest.php +++ b/tests/Unit/SDK/Metrics/MetricExporter/InMemoryExporterTest.php @@ -7,7 +7,9 @@ use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface; use OpenTelemetry\SDK\Metrics\Data\DataInterface; use OpenTelemetry\SDK\Metrics\Data\Metric; +use OpenTelemetry\SDK\Metrics\Data\Temporality; use OpenTelemetry\SDK\Metrics\MetricExporter\InMemoryExporter; +use OpenTelemetry\SDK\Metrics\MetricMetadataInterface; use OpenTelemetry\SDK\Resource\ResourceInfo; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -62,6 +64,50 @@ public function test_exporter_collect_returns_all_exported_metrics(): void $this->assertSame($metrics, $exporter->collect()); } + public function test_temporality_returns_configured_temporality(): void + { + $exporter = new InMemoryExporter(temporality: Temporality::DELTA); + $metric = $this->createMock(MetricMetadataInterface::class); + $metric->expects($this->never())->method('temporality'); + + $this->assertSame(Temporality::DELTA, $exporter->temporality($metric)); + } + + public function test_temporality_delegates_to_metric_when_not_configured(): void + { + $exporter = new InMemoryExporter(); + $metric = $this->createMock(MetricMetadataInterface::class); + $metric->expects($this->once())->method('temporality')->willReturn(Temporality::CUMULATIVE); + + $this->assertSame(Temporality::CUMULATIVE, $exporter->temporality($metric)); + } + + public function test_shutdown_returns_true(): void + { + $exporter = new InMemoryExporter(); + $this->assertTrue($exporter->shutdown()); + } + + public function test_shutdown_returns_false_when_already_closed(): void + { + $exporter = new InMemoryExporter(); + $exporter->shutdown(); + $this->assertFalse($exporter->shutdown()); + } + + public function test_export_returns_false_when_closed(): void + { + $exporter = new InMemoryExporter(); + $exporter->shutdown(); + $this->assertFalse($exporter->export([])); + } + + public function test_force_flush_returns_true(): void + { + $exporter = new InMemoryExporter(); + $this->assertTrue($exporter->forceFlush()); + } + /** * @return list */ diff --git a/tests/Unit/SDK/Metrics/MetricExporter/NoopMetricExporterTest.php b/tests/Unit/SDK/Metrics/MetricExporter/NoopMetricExporterTest.php new file mode 100644 index 000000000..1ed9932b8 --- /dev/null +++ b/tests/Unit/SDK/Metrics/MetricExporter/NoopMetricExporterTest.php @@ -0,0 +1,25 @@ +assertTrue($exporter->export([])); + } + + public function test_shutdown_returns_true(): void + { + $exporter = new NoopMetricExporter(); + $this->assertTrue($exporter->shutdown()); + } +} diff --git a/tests/Unit/SDK/Metrics/MetricReader/ExportingReaderTest.php b/tests/Unit/SDK/Metrics/MetricReader/ExportingReaderTest.php index dfe9a218f..ef36a05b5 100644 --- a/tests/Unit/SDK/Metrics/MetricReader/ExportingReaderTest.php +++ b/tests/Unit/SDK/Metrics/MetricReader/ExportingReaderTest.php @@ -13,16 +13,21 @@ use OpenTelemetry\SDK\Metrics\Data\Metric; use OpenTelemetry\SDK\Metrics\Data\Temporality; use OpenTelemetry\SDK\Metrics\DefaultAggregationProviderInterface; +use OpenTelemetry\SDK\Metrics\Instrument; use OpenTelemetry\SDK\Metrics\InstrumentType; use OpenTelemetry\SDK\Metrics\MetricExporter\InMemoryExporter; use OpenTelemetry\SDK\Metrics\MetricExporterInterface; +use OpenTelemetry\SDK\Metrics\MetricFactory\StreamMetricSourceProvider; use OpenTelemetry\SDK\Metrics\MetricMetadataInterface; use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader; +use OpenTelemetry\SDK\Metrics\MetricRegistry\MetricCollectorInterface; use OpenTelemetry\SDK\Metrics\MetricSourceInterface; use OpenTelemetry\SDK\Metrics\MetricSourceProviderInterface; use OpenTelemetry\SDK\Metrics\PushMetricExporterInterface; use OpenTelemetry\SDK\Metrics\StalenessHandler\ImmediateStalenessHandler; use OpenTelemetry\SDK\Metrics\StalenessHandlerInterface; +use OpenTelemetry\SDK\Metrics\Stream\MetricStreamInterface; +use OpenTelemetry\SDK\Metrics\ViewProjection; use OpenTelemetry\SDK\Resource\ResourceInfo; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -230,6 +235,147 @@ public function test_closed_reader_does_not_call_exporter_methods(): void $reader->shutdown(); $reader->forceFlush(); } + + public function test_unregister_stream_removes_source_from_collection(): void + { + $exporter = new InMemoryExporter(temporality: Temporality::CUMULATIVE); + $reader = new ExportingReader($exporter); + + $collector = $this->createMock(MetricCollectorInterface::class); + $stream = $this->createMock(MetricStreamInterface::class); + $stream->method('register')->willReturn(0); + $stream->method('temporality')->willReturn(Temporality::CUMULATIVE); + $stream->method('collect')->willReturn($this->createMock(DataInterface::class)); + $stream->method('timestamp')->willReturn(0); + + $provider = new StreamMetricSourceProvider( + view: new ViewProjection('test', null, null, null, null), + instrument: new Instrument(InstrumentType::COUNTER, 'test', null, null), + instrumentationLibrary: $this->createMock(InstrumentationScopeInterface::class), + resource: $this->createMock(ResourceInfo::class), + stream: $stream, + metricCollector: $collector, + streamId: 1, + ); + + $stalenessHandler = $this->createMock(StalenessHandlerInterface::class); + $reader->add($provider, $provider, $stalenessHandler); + + // Before unregister, collecting should produce a metric + $reader->collect(); + $metrics = $exporter->collect(true); + $this->assertCount(1, $metrics); + + // Unregister the stream + $reader->unregisterStream($collector, 1); + + // After unregister, collecting should produce no metrics + $reader->collect(); + $metrics = $exporter->collect(); + $this->assertSame([], $metrics); + } + + public function test_unregister_stream_cleans_up_registry_when_no_streams_remain(): void + { + $exporter = new InMemoryExporter(temporality: Temporality::CUMULATIVE); + $reader = new ExportingReader($exporter); + + $collector = $this->createMock(MetricCollectorInterface::class); + $stream = $this->createMock(MetricStreamInterface::class); + $stream->method('register')->willReturn(0); + $stream->method('temporality')->willReturn(Temporality::CUMULATIVE); + $stream->method('collect')->willReturn($this->createMock(DataInterface::class)); + $stream->method('timestamp')->willReturn(0); + + $provider = new StreamMetricSourceProvider( + view: new ViewProjection('test', null, null, null, null), + instrument: new Instrument(InstrumentType::COUNTER, 'test', null, null), + instrumentationLibrary: $this->createMock(InstrumentationScopeInterface::class), + resource: $this->createMock(ResourceInfo::class), + stream: $stream, + metricCollector: $collector, + streamId: 1, + ); + + $stalenessHandler = $this->createMock(StalenessHandlerInterface::class); + $reader->add($provider, $provider, $stalenessHandler); + + // Unregister the only stream for this collector + $reader->unregisterStream($collector, 1); + + // collectAndPush should never be called since the registry was cleaned up + $collector->expects($this->never())->method('collectAndPush'); + $reader->collect(); + $this->assertSame([], $exporter->collect()); + } + + public function test_unregister_stream_keeps_registry_when_other_streams_remain(): void + { + $exporter = new InMemoryExporter(temporality: Temporality::CUMULATIVE); + $reader = new ExportingReader($exporter); + + $collector = $this->createMock(MetricCollectorInterface::class); + + $stream1 = $this->createMock(MetricStreamInterface::class); + $stream1->method('register')->willReturn(0); + $stream1->method('temporality')->willReturn(Temporality::CUMULATIVE); + $stream1->method('collect')->willReturn($this->createMock(DataInterface::class)); + $stream1->method('timestamp')->willReturn(0); + + $stream2 = $this->createMock(MetricStreamInterface::class); + $stream2->method('register')->willReturn(1); + $stream2->method('temporality')->willReturn(Temporality::CUMULATIVE); + $stream2->method('collect')->willReturn($this->createMock(DataInterface::class)); + $stream2->method('timestamp')->willReturn(0); + + $provider1 = new StreamMetricSourceProvider( + view: new ViewProjection('test1', null, null, null, null), + instrument: new Instrument(InstrumentType::COUNTER, 'test1', null, null), + instrumentationLibrary: $this->createMock(InstrumentationScopeInterface::class), + resource: $this->createMock(ResourceInfo::class), + stream: $stream1, + metricCollector: $collector, + streamId: 1, + ); + + $provider2 = new StreamMetricSourceProvider( + view: new ViewProjection('test2', null, null, null, null), + instrument: new Instrument(InstrumentType::COUNTER, 'test2', null, null), + instrumentationLibrary: $this->createMock(InstrumentationScopeInterface::class), + resource: $this->createMock(ResourceInfo::class), + stream: $stream2, + metricCollector: $collector, + streamId: 2, + ); + + $stalenessHandler = $this->createMock(StalenessHandlerInterface::class); + $reader->add($provider1, $provider1, $stalenessHandler); + $reader->add($provider2, $provider2, $stalenessHandler); + + // Unregister only stream 1 - stream 2 should still remain + $reader->unregisterStream($collector, 1); + + // collectAndPush should still be called since stream 2 remains for this collector + $collector->expects($this->once())->method('collectAndPush'); + $reader->collect(); + $metrics = $exporter->collect(); + $this->assertCount(1, $metrics); + } + + public function test_unregister_stream_with_nonexistent_stream_is_noop(): void + { + $exporter = new InMemoryExporter(temporality: Temporality::CUMULATIVE); + $reader = new ExportingReader($exporter); + + $collector = $this->createMock(MetricCollectorInterface::class); + + // Unregistering a stream that was never registered triggers a PHP warning + // due to accessing an undefined array key in the cleanup check + @$reader->unregisterStream($collector, 999); + + $reader->collect(); + $this->assertSame([], $exporter->collect()); + } } interface DefaultAggregationProviderExporterInterface extends MetricExporterInterface, DefaultAggregationProviderInterface diff --git a/tests/Unit/SDK/Metrics/MetricRegistry/MetricRegistryTest.php b/tests/Unit/SDK/Metrics/MetricRegistry/MetricRegistryTest.php index 6a9cc6849..5b8c76908 100644 --- a/tests/Unit/SDK/Metrics/MetricRegistry/MetricRegistryTest.php +++ b/tests/Unit/SDK/Metrics/MetricRegistry/MetricRegistryTest.php @@ -152,4 +152,40 @@ public function test_collect_and_push_multi_instrument_callback_collects_only_sp new NumberDataPoint(0, Attributes::create([]), 5, 7), ], Temporality::DELTA, true), $stream1->collect($reader1)); } + + public function test_unregister_streams_removes_instrument_streams(): void + { + $registry = new MetricRegistry(null, Attributes::factory(), new TestClock(1)); + $stream = new SynchronousMetricStream(new SumAggregation(true), 0); + $instrument = new Instrument(InstrumentType::COUNTER, 'test', null, null); + + $streamId = $registry->registerSynchronousStream($instrument, $stream, new MetricAggregator(null, new SumAggregation(true))); + $this->assertTrue($registry->enabled($instrument)); + + $streamIds = $registry->unregisterStreams($instrument); + $this->assertSame([$streamId => $streamId], $streamIds); + $this->assertFalse($registry->enabled($instrument)); + } + + public function test_enabled_returns_false_for_unregistered_instrument(): void + { + $registry = new MetricRegistry(null, Attributes::factory(), new TestClock(1)); + $instrument = new Instrument(InstrumentType::COUNTER, 'test', null, null); + $this->assertFalse($registry->enabled($instrument)); + } + + public function test_unregister_callback_removes_callback(): void + { + $this->expectOutputString(''); + + $registry = new MetricRegistry(null, Attributes::factory(), new TestClock()); + $stream = new AsynchronousMetricStream(new SumAggregation(true), 0); + $instrument = new Instrument(InstrumentType::ASYNCHRONOUS_COUNTER, 'test', null, null); + + $streamId = $registry->registerAsynchronousStream($instrument, $stream, new MetricAggregatorFactory(null, new SumAggregation(true))); + $callbackId = $registry->registerCallback(fn (ObserverInterface $o) => printf('called'), $instrument); + $registry->unregisterCallback($callbackId); + + $registry->collectAndPush([$streamId]); + } } diff --git a/tests/Unit/SDK/Metrics/NoopMeterProviderTest.php b/tests/Unit/SDK/Metrics/NoopMeterProviderTest.php new file mode 100644 index 000000000..c21236218 --- /dev/null +++ b/tests/Unit/SDK/Metrics/NoopMeterProviderTest.php @@ -0,0 +1,47 @@ +assertTrue($provider->shutdown()); + } + + public function test_force_flush_returns_true(): void + { + $provider = new NoopMeterProvider(); + $this->assertTrue($provider->forceFlush()); + } + + public function test_get_meter_returns_meter(): void + { + $provider = new NoopMeterProvider(); + $this->assertInstanceOf(MeterInterface::class, $provider->getMeter('test')); + } + + public function test_get_meter_with_all_params(): void + { + $provider = new NoopMeterProvider(); + $meter = $provider->getMeter('test', '1.0', 'https://schema', []); + $this->assertInstanceOf(MeterInterface::class, $meter); + } + + public function test_update_configurator_does_nothing(): void + { + $provider = new NoopMeterProvider(); + $provider->updateConfigurator(Configurator::meter()); + $this->assertTrue(true); + } +} diff --git a/tests/Unit/SDK/Metrics/ObservableCallbackDestructorTest.php b/tests/Unit/SDK/Metrics/ObservableCallbackDestructorTest.php new file mode 100644 index 000000000..31de35d72 --- /dev/null +++ b/tests/Unit/SDK/Metrics/ObservableCallbackDestructorTest.php @@ -0,0 +1,68 @@ +createMock(MetricWriterInterface::class); + $writer->expects($this->exactly(2)) + ->method('unregisterCallback') + ->willReturnCallback(function (int $id): void { + $this->assertContains($id, [1, 2]); + }); + + $counter1 = $this->createMock(ReferenceCounterInterface::class); + $counter1->expects($this->once())->method('release'); + + $counter2 = $this->createMock(ReferenceCounterInterface::class); + $counter2->expects($this->once())->method('release'); + + /** @var ArrayAccess $destructors */ + $destructors = new WeakMap(); + + $destructor = new ObservableCallbackDestructor($destructors, $writer); + $destructor->callbackIds[1] = $counter1; + $destructor->callbackIds[2] = $counter2; + + // Trigger __destruct + unset($destructor); + } + + public function test_destruct_with_no_callbacks(): void + { + $writer = $this->createMock(MetricWriterInterface::class); + $writer->expects($this->never())->method('unregisterCallback'); + + /** @var ArrayAccess $destructors */ + $destructors = new WeakMap(); + + $destructor = new ObservableCallbackDestructor($destructors, $writer); + + // Should not throw + unset($destructor); + } + + public function test_callback_ids_is_initially_empty(): void + { + $writer = $this->createMock(MetricWriterInterface::class); + /** @var ArrayAccess $destructors */ + $destructors = new WeakMap(); + + $destructor = new ObservableCallbackDestructor($destructors, $writer); + + $this->assertEmpty($destructor->callbackIds); + } +} diff --git a/tests/Unit/SDK/Metrics/StalenessHandler/MultiReferenceCounterTest.php b/tests/Unit/SDK/Metrics/StalenessHandler/MultiReferenceCounterTest.php new file mode 100644 index 000000000..f647085e9 --- /dev/null +++ b/tests/Unit/SDK/Metrics/StalenessHandler/MultiReferenceCounterTest.php @@ -0,0 +1,59 @@ +createMock(ReferenceCounterInterface::class); + $counter1->expects($this->once())->method('acquire')->with(false); + + $counter2 = $this->createMock(ReferenceCounterInterface::class); + $counter2->expects($this->once())->method('acquire')->with(false); + + $multi = new MultiReferenceCounter([$counter1, $counter2]); + $multi->acquire(); + } + + public function test_acquire_persistent_delegates_to_all_counters(): void + { + $counter1 = $this->createMock(ReferenceCounterInterface::class); + $counter1->expects($this->once())->method('acquire')->with(true); + + $counter2 = $this->createMock(ReferenceCounterInterface::class); + $counter2->expects($this->once())->method('acquire')->with(true); + + $multi = new MultiReferenceCounter([$counter1, $counter2]); + $multi->acquire(true); + } + + public function test_release_delegates_to_all_counters(): void + { + $counter1 = $this->createMock(ReferenceCounterInterface::class); + $counter1->expects($this->once())->method('release'); + + $counter2 = $this->createMock(ReferenceCounterInterface::class); + $counter2->expects($this->once())->method('release'); + + $multi = new MultiReferenceCounter([$counter1, $counter2]); + $multi->release(); + } + + public function test_empty_counters_does_not_throw(): void + { + $multi = new MultiReferenceCounter([]); + $multi->acquire(); + $multi->release(); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/SDK/Metrics/StalenessHandler/NoopStalenessHandlerFactoryTest.php b/tests/Unit/SDK/Metrics/StalenessHandler/NoopStalenessHandlerFactoryTest.php new file mode 100644 index 000000000..a2483e70c --- /dev/null +++ b/tests/Unit/SDK/Metrics/StalenessHandler/NoopStalenessHandlerFactoryTest.php @@ -0,0 +1,63 @@ +create(); + + $this->assertInstanceOf(NoopStalenessHandler::class, $handler); + $this->assertInstanceOf(ReferenceCounterInterface::class, $handler); + $this->assertInstanceOf(StalenessHandlerInterface::class, $handler); + } + + public function test_create_returns_same_instance(): void + { + $factory = new NoopStalenessHandlerFactory(); + + $this->assertSame($factory->create(), $factory->create()); + } + + public function test_noop_handler_acquire_and_release_do_not_throw(): void + { + $factory = new NoopStalenessHandlerFactory(); + $handler = $factory->create(); + + $handler->acquire(); + $handler->acquire(true); + $handler->release(); + + $this->addToAssertionCount(1); + } + + public function test_noop_handler_on_stale_does_not_invoke_callback(): void + { + $factory = new NoopStalenessHandlerFactory(); + $handler = $factory->create(); + + $called = false; + $handler->onStale(static function () use (&$called): void { + $called = true; + }); + + $handler->acquire(); + $handler->release(); + + /** @phpstan-ignore-next-line */ + $this->assertFalse($called); + } +} diff --git a/tests/Unit/SDK/Propagation/LateBindingTextMapPropagatorTest.php b/tests/Unit/SDK/Propagation/LateBindingTextMapPropagatorTest.php new file mode 100644 index 000000000..82d9ba082 --- /dev/null +++ b/tests/Unit/SDK/Propagation/LateBindingTextMapPropagatorTest.php @@ -0,0 +1,85 @@ +createMock(TextMapPropagatorInterface::class); + $inner->expects($this->once())->method('fields')->willReturn(['traceparent']); + $propagator = new LateBindingTextMapPropagator($inner); + $this->assertSame(['traceparent'], $propagator->fields()); + } + + public function test_fields_resolves_closure(): void + { + $inner = $this->createMock(TextMapPropagatorInterface::class); + $inner->expects($this->once())->method('fields')->willReturn(['traceparent']); + $propagator = new LateBindingTextMapPropagator(fn () => $inner); + $this->assertSame(['traceparent'], $propagator->fields()); + } + + public function test_inject_delegates_to_propagator(): void + { + $inner = $this->createMock(TextMapPropagatorInterface::class); + $inner->expects($this->once())->method('inject'); + $propagator = new LateBindingTextMapPropagator($inner); + $carrier = []; + $propagator->inject($carrier); + } + + public function test_inject_resolves_closure(): void + { + $inner = $this->createMock(TextMapPropagatorInterface::class); + $inner->expects($this->once())->method('inject'); + $propagator = new LateBindingTextMapPropagator(fn () => $inner); + $carrier = []; + $propagator->inject($carrier); + } + + public function test_extract_delegates_to_propagator(): void + { + $context = Context::getRoot(); + $inner = $this->createMock(TextMapPropagatorInterface::class); + $inner->expects($this->once())->method('extract')->willReturn($context); + $propagator = new LateBindingTextMapPropagator($inner); + $this->assertSame($context, $propagator->extract([])); + } + + public function test_extract_resolves_closure(): void + { + $context = Context::getRoot(); + $inner = $this->createMock(TextMapPropagatorInterface::class); + $inner->expects($this->once())->method('extract')->willReturn($context); + $propagator = new LateBindingTextMapPropagator(fn () => $inner); + $this->assertSame($context, $propagator->extract([])); + } + + public function test_closure_called_only_once(): void + { + $callCount = 0; + $inner = $this->createMock(TextMapPropagatorInterface::class); + $inner->method('fields')->willReturn([]); + $inner->method('extract')->willReturn(Context::getRoot()); + $propagator = new LateBindingTextMapPropagator(function () use ($inner, &$callCount) { + $callCount++; + + return $inner; + }); + $propagator->fields(); + $propagator->extract([]); + $carrier = []; + $propagator->inject($carrier); + $this->assertSame(1, $callCount); + } +} diff --git a/tests/Unit/SDK/RegistryTest.php b/tests/Unit/SDK/RegistryTest.php new file mode 100644 index 000000000..91bdbec4a --- /dev/null +++ b/tests/Unit/SDK/RegistryTest.php @@ -0,0 +1,258 @@ +getProperty($prop); + self::$originalState[$prop] = $rp->getValue(); + } + } + + protected function setUp(): void + { + $reflection = new ReflectionClass(Registry::class); + foreach (self::$originalState as $prop => $value) { + $rp = $reflection->getProperty($prop); + $rp->setValue(null, []); + } + } + + public static function tearDownAfterClass(): void + { + $reflection = new ReflectionClass(Registry::class); + foreach (self::$originalState as $prop => $value) { + $rp = $reflection->getProperty($prop); + $rp->setValue(null, $value); + } + } + + public function test_register_transport_factory_with_object(): void + { + $factory = $this->createMock(TransportFactoryInterface::class); + Registry::registerTransportFactory('http', $factory, true); + $this->assertInstanceOf(TransportFactoryInterface::class, Registry::transportFactory('http')); + } + + public function test_register_transport_factory_clobber_false_skips_existing(): void + { + $first = $this->createMock(TransportFactoryInterface::class); + $second = $this->createMock(TransportFactoryInterface::class); + Registry::registerTransportFactory('http', $first, true); + Registry::registerTransportFactory('http', $second, false); + $result = Registry::transportFactory('http'); + $this->assertNotSame($second, $result); + $this->assertInstanceOf(TransportFactoryInterface::class, $result); + } + + public function test_register_transport_factory_type_error_for_invalid_class(): void + { + $this->expectException(TypeError::class); + Registry::registerTransportFactory('http', \stdClass::class, true); + } + + public function test_register_span_exporter_factory_with_object(): void + { + $factory = $this->createMock(SpanExporterFactoryInterface::class); + Registry::registerSpanExporterFactory('test', $factory, true); + $this->assertInstanceOf(SpanExporterFactoryInterface::class, Registry::spanExporterFactory('test')); + } + + public function test_register_span_exporter_factory_type_error(): void + { + $this->expectException(TypeError::class); + Registry::registerSpanExporterFactory('test', \stdClass::class, true); + } + + public function test_register_metric_exporter_factory_with_object(): void + { + $factory = $this->createMock(MetricExporterFactoryInterface::class); + Registry::registerMetricExporterFactory('test', $factory, true); + $this->assertInstanceOf(MetricExporterFactoryInterface::class, Registry::metricExporterFactory('test')); + } + + public function test_register_metric_exporter_factory_type_error(): void + { + $this->expectException(TypeError::class); + Registry::registerMetricExporterFactory('test', \stdClass::class, true); + } + + public function test_register_log_record_exporter_factory_with_object(): void + { + $factory = $this->createMock(LogRecordExporterFactoryInterface::class); + Registry::registerLogRecordExporterFactory('test', $factory, true); + $this->assertInstanceOf(LogRecordExporterFactoryInterface::class, Registry::logRecordExporterFactory('test')); + } + + public function test_register_log_record_exporter_factory_type_error(): void + { + $this->expectException(TypeError::class); + Registry::registerLogRecordExporterFactory('test', \stdClass::class, true); + } + + public function test_register_and_retrieve_text_map_propagator(): void + { + $propagator = $this->createMock(TextMapPropagatorInterface::class); + Registry::registerTextMapPropagator('test', $propagator); + $this->assertSame($propagator, Registry::textMapPropagator('test')); + } + + public function test_register_and_retrieve_resource_detector(): void + { + $detector = $this->createMock(ResourceDetectorInterface::class); + Registry::registerResourceDetector('test', $detector); + $this->assertSame($detector, Registry::resourceDetector('test')); + } + + public function test_register_and_retrieve_response_propagator(): void + { + $propagator = $this->createMock(ResponsePropagatorInterface::class); + Registry::registerResponsePropagator('test', $propagator); + $this->assertSame($propagator, Registry::responsePropagator('test')); + } + + public function test_span_exporter_factory_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::spanExporterFactory('nonexistent'); + } + + public function test_log_record_exporter_factory_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::logRecordExporterFactory('nonexistent'); + } + + public function test_transport_factory_parses_protocol_with_content_type(): void + { + $factory = $this->createMock(TransportFactoryInterface::class); + Registry::registerTransportFactory('http', $factory, true); + $this->assertInstanceOf(TransportFactoryInterface::class, Registry::transportFactory('http/json')); + } + + public function test_transport_factory_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::transportFactory('nonexistent'); + } + + public function test_metric_exporter_factory_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::metricExporterFactory('nonexistent'); + } + + public function test_text_map_propagator_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::textMapPropagator('nonexistent'); + } + + public function test_resource_detector_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::resourceDetector('nonexistent'); + } + + public function test_response_propagator_throws_runtime_exception_for_missing(): void + { + $this->expectException(RuntimeException::class); + Registry::responsePropagator('nonexistent'); + } + + public function test_resource_detectors_returns_array(): void + { + $detector1 = $this->createMock(ResourceDetectorInterface::class); + $detector2 = $this->createMock(ResourceDetectorInterface::class); + Registry::registerResourceDetector('one', $detector1); + Registry::registerResourceDetector('two', $detector2); + $detectors = Registry::resourceDetectors(); + $this->assertIsArray($detectors); + $this->assertCount(2, $detectors); + $this->assertSame($detector1, $detectors[0]); + $this->assertSame($detector2, $detectors[1]); + } + + public function test_register_span_exporter_factory_clobber_false_skips_existing(): void + { + $first = $this->createMock(SpanExporterFactoryInterface::class); + $second = $this->createMock(SpanExporterFactoryInterface::class); + Registry::registerSpanExporterFactory('test-clobber', $first, true); + Registry::registerSpanExporterFactory('test-clobber', $second, false); + $result = Registry::spanExporterFactory('test-clobber'); + $this->assertNotSame($second, $result); + $this->assertInstanceOf(SpanExporterFactoryInterface::class, $result); + } + + public function test_register_metric_exporter_factory_clobber_false_skips_existing(): void + { + $first = $this->createMock(MetricExporterFactoryInterface::class); + $second = $this->createMock(MetricExporterFactoryInterface::class); + Registry::registerMetricExporterFactory('test-clobber', $first, true); + Registry::registerMetricExporterFactory('test-clobber', $second, false); + $result = Registry::metricExporterFactory('test-clobber'); + $this->assertNotSame($second, $result); + $this->assertInstanceOf(MetricExporterFactoryInterface::class, $result); + } + + public function test_register_log_record_exporter_factory_clobber_false_skips_existing(): void + { + $first = $this->createMock(LogRecordExporterFactoryInterface::class); + $second = $this->createMock(LogRecordExporterFactoryInterface::class); + Registry::registerLogRecordExporterFactory('test-clobber', $first, true); + Registry::registerLogRecordExporterFactory('test-clobber', $second, false); + $result = Registry::logRecordExporterFactory('test-clobber'); + $this->assertNotSame($second, $result); + $this->assertInstanceOf(LogRecordExporterFactoryInterface::class, $result); + } + + public function test_register_text_map_propagator_clobber_false_skips_existing(): void + { + $first = $this->createMock(TextMapPropagatorInterface::class); + $second = $this->createMock(TextMapPropagatorInterface::class); + Registry::registerTextMapPropagator('test', $first, true); + Registry::registerTextMapPropagator('test', $second, false); + $this->assertSame($first, Registry::textMapPropagator('test')); + } + + public function test_register_response_propagator_clobber_false_skips_existing(): void + { + $first = $this->createMock(ResponsePropagatorInterface::class); + $second = $this->createMock(ResponsePropagatorInterface::class); + Registry::registerResponsePropagator('test', $first, true); + Registry::registerResponsePropagator('test', $second, false); + $this->assertSame($first, Registry::responsePropagator('test')); + } +} diff --git a/tests/Unit/SDK/Resource/Detectors/SdkProvidedTest.php b/tests/Unit/SDK/Resource/Detectors/SdkProvidedTest.php new file mode 100644 index 000000000..95cce659d --- /dev/null +++ b/tests/Unit/SDK/Resource/Detectors/SdkProvidedTest.php @@ -0,0 +1,23 @@ +getResource(); + + $this->assertInstanceOf(ResourceInfo::class, $resource); + $this->assertCount(0, $resource->getAttributes()); + } +} diff --git a/tests/Unit/SDK/Trace/SpanLimitsBuilderTest.php b/tests/Unit/SDK/Trace/SpanLimitsBuilderTest.php index fcaea3808..04ae3c2ed 100644 --- a/tests/Unit/SDK/Trace/SpanLimitsBuilderTest.php +++ b/tests/Unit/SDK/Trace/SpanLimitsBuilderTest.php @@ -51,4 +51,44 @@ public function test_span_limits_builder_throws_exception_on_invalid_value_from_ $this->expectException(Exception::class); $builder->build(); } + + public function test_span_limits_builder_set_event_count_limit(): void + { + $builder = new SpanLimitsBuilder(); + $builder->setEventCountLimit(50); + $spanLimits = $builder->build(); + $this->assertSame(50, $spanLimits->getEventCountLimit()); + } + + public function test_span_limits_builder_set_link_count_limit(): void + { + $builder = new SpanLimitsBuilder(); + $builder->setLinkCountLimit(64); + $spanLimits = $builder->build(); + $this->assertSame(64, $spanLimits->getLinkCountLimit()); + } + + public function test_span_limits_builder_set_attribute_per_event_count_limit(): void + { + $builder = new SpanLimitsBuilder(); + $builder->setAttributePerEventCountLimit(32); + $spanLimits = $builder->build(); + $this->assertEquals(Attributes::factory(32), $spanLimits->getEventAttributesFactory()); + } + + public function test_span_limits_builder_set_attribute_per_link_count_limit(): void + { + $builder = new SpanLimitsBuilder(); + $builder->setAttributePerLinkCountLimit(48); + $spanLimits = $builder->build(); + $this->assertEquals(Attributes::factory(48), $spanLimits->getLinkAttributesFactory()); + } + + public function test_span_limits_builder_set_attribute_value_length_limit(): void + { + $builder = new SpanLimitsBuilder(); + $builder->setAttributeValueLengthLimit(100); + $spanLimits = $builder->build(); + $this->assertEquals(Attributes::factory(128, 100), $spanLimits->getAttributesFactory()); + } } diff --git a/tests/Unit/SDK/Trace/SpanLimitsTest.php b/tests/Unit/SDK/Trace/SpanLimitsTest.php new file mode 100644 index 000000000..cc58ffbe4 --- /dev/null +++ b/tests/Unit/SDK/Trace/SpanLimitsTest.php @@ -0,0 +1,53 @@ +assertSame($attrFactory, $limits->getAttributesFactory()); + } + + public function test_get_event_attributes_factory(): void + { + $attrFactory = Attributes::factory(128); + $eventFactory = Attributes::factory(64); + $linkFactory = Attributes::factory(32); + $limits = new SpanLimits($attrFactory, $eventFactory, $linkFactory, 128, 128); + $this->assertSame($eventFactory, $limits->getEventAttributesFactory()); + } + + public function test_get_link_attributes_factory(): void + { + $attrFactory = Attributes::factory(128); + $eventFactory = Attributes::factory(64); + $linkFactory = Attributes::factory(32); + $limits = new SpanLimits($attrFactory, $eventFactory, $linkFactory, 128, 128); + $this->assertSame($linkFactory, $limits->getLinkAttributesFactory()); + } + + public function test_get_event_count_limit(): void + { + $limits = new SpanLimits(Attributes::factory(), Attributes::factory(), Attributes::factory(), 50, 64); + $this->assertSame(50, $limits->getEventCountLimit()); + } + + public function test_get_link_count_limit(): void + { + $limits = new SpanLimits(Attributes::factory(), Attributes::factory(), Attributes::factory(), 128, 64); + $this->assertSame(64, $limits->getLinkCountLimit()); + } +} diff --git a/tests/Unit/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/WildcardPatternTest.php b/tests/Unit/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/WildcardPatternTest.php new file mode 100644 index 000000000..de436b2b5 --- /dev/null +++ b/tests/Unit/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/WildcardPatternTest.php @@ -0,0 +1,70 @@ +add('exact'); + $this->assertTrue($pattern->matches('exact')); + } + + public function test_static_no_match(): void + { + $pattern = new WildcardPattern(); + $pattern->add('exact'); + $this->assertFalse($pattern->matches('other')); + } + + public function test_wildcard_star(): void + { + $pattern = new WildcardPattern(); + $pattern->add('foo*'); + $this->assertTrue($pattern->matches('foobar')); + $this->assertTrue($pattern->matches('foo')); + $this->assertFalse($pattern->matches('barfoo')); + } + + public function test_wildcard_question(): void + { + $pattern = new WildcardPattern(); + $pattern->add('fo?'); + $this->assertTrue($pattern->matches('foo')); + $this->assertTrue($pattern->matches('fob')); + $this->assertFalse($pattern->matches('fooo')); + } + + public function test_wildcard_middle(): void + { + $pattern = new WildcardPattern(); + $pattern->add('foo*bar'); + $this->assertTrue($pattern->matches('foobar')); + $this->assertTrue($pattern->matches('fooxyzbar')); + $this->assertFalse($pattern->matches('foobarbaz')); + } + + public function test_no_patterns_no_match(): void + { + $pattern = new WildcardPattern(); + $this->assertFalse($pattern->matches('anything')); + } + + public function test_multiple_patterns(): void + { + $pattern = new WildcardPattern(); + $pattern->add('foo'); + $pattern->add('bar*'); + $this->assertTrue($pattern->matches('foo')); + $this->assertTrue($pattern->matches('barbaz')); + $this->assertFalse($pattern->matches('qux')); + } +} diff --git a/tests/Unit/SDK/Trace/StatusDataTest.php b/tests/Unit/SDK/Trace/StatusDataTest.php index da3cdab1b..25c04c7fa 100644 --- a/tests/Unit/SDK/Trace/StatusDataTest.php +++ b/tests/Unit/SDK/Trace/StatusDataTest.php @@ -74,4 +74,40 @@ public static function getStatuses(): array [StatusCode::STATUS_UNSET], ]; } + + public function test_create_with_description_ignores_description_for_ok(): void + { + $status = StatusData::create(StatusCode::STATUS_OK, 'some description'); + $this->assertSame(StatusCode::STATUS_OK, $status->getCode()); + $this->assertSame('', $status->getDescription()); + } + + public function test_create_with_description_ignores_description_for_unset(): void + { + $status = StatusData::create(StatusCode::STATUS_UNSET, 'some description'); + $this->assertSame(StatusCode::STATUS_UNSET, $status->getCode()); + $this->assertSame('', $status->getDescription()); + } + + public function test_create_with_null_description(): void + { + $status = StatusData::create(StatusCode::STATUS_ERROR); + $this->assertSame(StatusCode::STATUS_ERROR, $status->getCode()); + $this->assertSame('', $status->getDescription()); + } + + public function test_ok_returns_same_instance(): void + { + $this->assertSame(StatusData::ok(), StatusData::ok()); + } + + public function test_error_returns_same_instance(): void + { + $this->assertSame(StatusData::error(), StatusData::error()); + } + + public function test_unset_returns_same_instance(): void + { + $this->assertSame(StatusData::unset(), StatusData::unset()); + } } diff --git a/tests/Unit/SDK/Trace/TracerConfigTest.php b/tests/Unit/SDK/Trace/TracerConfigTest.php new file mode 100644 index 000000000..2a70ebe5c --- /dev/null +++ b/tests/Unit/SDK/Trace/TracerConfigTest.php @@ -0,0 +1,46 @@ +assertInstanceOf(TracerConfig::class, $config); + $this->assertInstanceOf(Config::class, $config); + } + + public function test_is_enabled_by_default(): void + { + $config = TracerConfig::default(); + + $this->assertTrue($config->isEnabled()); + } + + public function test_set_disabled(): void + { + $config = TracerConfig::default(); + $config->setDisabled(true); + + $this->assertFalse($config->isEnabled()); + } + + public function test_set_disabled_false_re_enables(): void + { + $config = TracerConfig::default(); + $config->setDisabled(true); + $config->setDisabled(false); + + $this->assertTrue($config->isEnabled()); + } +} diff --git a/tests/Unit/SDK/Trace/TracerProviderBuilderTest.php b/tests/Unit/SDK/Trace/TracerProviderBuilderTest.php new file mode 100644 index 000000000..501ca5eb8 --- /dev/null +++ b/tests/Unit/SDK/Trace/TracerProviderBuilderTest.php @@ -0,0 +1,66 @@ +build(); + $this->assertInstanceOf(TracerProviderInterface::class, $provider); + } + + public function test_add_span_processor(): void + { + $builder = new TracerProviderBuilder(); + $result = $builder->addSpanProcessor($this->createMock(SpanProcessorInterface::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(TracerProviderInterface::class, $builder->build()); + } + + public function test_set_resource(): void + { + $builder = new TracerProviderBuilder(); + $result = $builder->setResource($this->createMock(ResourceInfo::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(TracerProviderInterface::class, $builder->build()); + } + + public function test_set_sampler(): void + { + $builder = new TracerProviderBuilder(); + $result = $builder->setSampler($this->createMock(SamplerInterface::class)); + $this->assertSame($builder, $result); + $this->assertInstanceOf(TracerProviderInterface::class, $builder->build()); + } + + public function test_set_configurator(): void + { + $builder = new TracerProviderBuilder(); + $result = $builder->setConfigurator(Configurator::tracer()); + $this->assertSame($builder, $result); + $this->assertInstanceOf(TracerProviderInterface::class, $builder->build()); + } + + public function test_set_span_suppression_strategy(): void + { + $builder = new TracerProviderBuilder(); + $result = $builder->setSpanSuppressionStrategy(new NoopSuppressionStrategy()); + $this->assertSame($builder, $result); + $this->assertInstanceOf(TracerProviderInterface::class, $builder->build()); + } +}