<commit_before># frozen_string_literal: true

require "test_helper"
require "judoscale/reporter"
require "judoscale/config"

module Judoscale
  class TestJobMetricsCollector < JobMetricsCollector
  end

  class TestWebMetricsCollector < WebMetricsCollector
    def collect
      [Metric.new(:qt, 1, Time.now)]
    end
  end

  describe Reporter do
    before {
      Judoscale.configure do |config|
        config.dyno = "web.1"
        config.api_base_url = "http://example.com/api/test-token"
      end
    }

    describe ".start" do
      before {
        Judoscale.add_adapter :test_web, {}, metrics_collector: TestWebMetricsCollector
        Judoscale.add_adapter :test_job, {}, metrics_collector: TestJobMetricsCollector
      }

      after {
        Judoscale.remove_adapter :test_web
        Judoscale.remove_adapter :test_job
      }

      it "initializes the reporter with all registered web and job metrics collectors when on the first dyno" do
        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, false
        reporter_mock.expect :start!, true do |config, metrics_collectors|
          _(metrics_collectors.size).must_equal 2
          _(metrics_collectors[0]).must_be_instance_of TestWebMetricsCollector
          _(metrics_collectors[1]).must_be_instance_of TestJobMetricsCollector
        end

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end

      it "initializes the reporter only with registered web metrics collectors on subsequent dynos to avoid redundant worker metrics" do
        Judoscale.configure { |config| config.dyno = "web.2" }

        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, false
        reporter_mock.expect :start!, true do |config, metrics_collectors|
          _(metrics_collectors.size).must_equal 1
          _(metrics_collectors[0]).must_be_instance_of TestWebMetricsCollector
        end

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end

      it "initializes the reporter only with registered job metrics collectors on the first non-web dyno to avoid unnecessary web collection attempts" do
        Judoscale.configure { |config| config.dyno = "worker.1" }

        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, false
        reporter_mock.expect :start!, true do |config, metrics_collectors|
          _(metrics_collectors.size).must_equal 1
          _(metrics_collectors[0]).must_be_instance_of TestJobMetricsCollector
        end

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end

      it "does not initialize the reporter more than once" do
        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, true

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end
    end

    describe "#start!" do
      after {
        Reporter.instance.stop!
      }

      def run_reporter_start_thread(collectors: [TestWebMetricsCollector.new])
        stub_reporter_loop {
          reporter_thread = Reporter.instance.start!(Config.instance, collectors)
          reporter_thread.join
        }
      end

      def stub_reporter_loop
        Reporter.instance.stub(:loop, ->(&blk) { blk.call }) {
          Reporter.instance.stub(:sleep, true) {
            yield
          }
        }
      end

      it "sends a report with collected metrics" do
        metrics_collector = TestWebMetricsCollector.new
        metrics = metrics_collector.collect

        expected_body = Report.new(Judoscale.adapters, Config.instance, metrics).as_json
        stub = stub_request(:post, "http://example.com/api/test-token/adapter/v1/metrics")
          .with(body: JSON.generate(expected_body))

        run_reporter_start_thread(collectors: [metrics_collector])

        assert_requested stub
      end

      it "logs exceptions when reporting collected information" do
        Reporter.instance.stub(:report!, ->(*) { raise "REPORT BOOM!" }) {
          run_reporter_start_thread
        }

        _(log_string).must_include "Reporter error: #<RuntimeError: REPORT BOOM!>"
        _(log_string).must_include "lib/judoscale/reporter.rb"
      end

      it "logs exceptions when collecting information" do
        metrics_collector = TestWebMetricsCollector.new

        metrics_collector.stub(:collect, ->(*) { raise "ADAPTER BOOM!" }) {
          run_reporter_start_thread(collectors: [metrics_collector])
        }

        _(log_string).must_include "Reporter error: #<RuntimeError: ADAPTER BOOM!>"
        _(log_string).must_include "lib/judoscale/reporter.rb"
      end

      it "does not run the reporter thread when the API url is not configured" do
        Judoscale.configure { |config| config.api_base_url = nil }

        Thread.stub(:new, ->(*) { raise "SHOULD NOT BE CALLED" }) {
          Reporter.instance.start!(Config.instance, [TestWebMetricsCollector.new])
        }

        _(log_string).must_include "Reporter not started: JUDOSCALE_URL is not set"
      end

      it "does not run the reporter thread when there are no metrics collectors" do
        Thread.stub(:new, ->(*) { raise "SHOULD NOT BE CALLED" }) {
          Reporter.instance.start!(Config.instance, [])
        }

        _(log_string).must_include "Reporter not started: no metrics need to be collected on this dyno"
      end
    end

    describe "#report!" do
      it "reports collected metrics to the API" do
        metrics = [
          Metric.new(:qt, 11, Time.at(1_000_000_001)), # web metric
          Metric.new(:qt, 22, Time.at(1_000_000_002), "high") # worker metric
        ]

        expected_body = Report.new(Judoscale.adapters, Config.instance, metrics).as_json
        stub = stub_request(:post, "http://example.com/api/test-token/adapter/v1/metrics")
          .with(body: JSON.generate(expected_body))

        Reporter.instance.send :report!, Config.instance, metrics

        assert_requested stub
      end

      it "logs reporter failures" do
        stub_request(:post, %r{http://example.com/api/test-token/adapter/v1/metrics})
          .to_return(body: "oops", status: 503)<commit_msg>Add test for reporter starting successfully logs<commit_after># frozen_string_literal: true

require "test_helper"
require "judoscale/reporter"
require "judoscale/config"

module Judoscale
  class TestJobMetricsCollector < JobMetricsCollector
  end

  class TestWebMetricsCollector < WebMetricsCollector
    def collect
      [Metric.new(:qt, 1, Time.now)]
    end
  end

  describe Reporter do
    before {
      Judoscale.configure do |config|
        config.dyno = "web.1"
        config.api_base_url = "http://example.com/api/test-token"
      end
    }

    describe ".start" do
      before {
        Judoscale.add_adapter :test_web, {}, metrics_collector: TestWebMetricsCollector
        Judoscale.add_adapter :test_job, {}, metrics_collector: TestJobMetricsCollector
      }

      after {
        Judoscale.remove_adapter :test_web
        Judoscale.remove_adapter :test_job
      }

      it "initializes the reporter with all registered web and job metrics collectors when on the first dyno" do
        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, false
        reporter_mock.expect :start!, true do |config, metrics_collectors|
          _(metrics_collectors.size).must_equal 2
          _(metrics_collectors[0]).must_be_instance_of TestWebMetricsCollector
          _(metrics_collectors[1]).must_be_instance_of TestJobMetricsCollector
        end

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end

      it "initializes the reporter only with registered web metrics collectors on subsequent dynos to avoid redundant worker metrics" do
        Judoscale.configure { |config| config.dyno = "web.2" }

        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, false
        reporter_mock.expect :start!, true do |config, metrics_collectors|
          _(metrics_collectors.size).must_equal 1
          _(metrics_collectors[0]).must_be_instance_of TestWebMetricsCollector
        end

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end

      it "initializes the reporter only with registered job metrics collectors on the first non-web dyno to avoid unnecessary web collection attempts" do
        Judoscale.configure { |config| config.dyno = "worker.1" }

        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, false
        reporter_mock.expect :start!, true do |config, metrics_collectors|
          _(metrics_collectors.size).must_equal 1
          _(metrics_collectors[0]).must_be_instance_of TestJobMetricsCollector
        end

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end

      it "does not initialize the reporter more than once" do
        reporter_mock = Minitest::Mock.new
        reporter_mock.expect :started?, true

        Reporter.stub(:instance, reporter_mock) {
          Reporter.start(Config.instance)
        }

        assert_mock reporter_mock
      end
    end

    describe "#start!" do
      after {
        Reporter.instance.stop!
      }

      def run_reporter_start_thread(collectors: [TestWebMetricsCollector.new])
        stub_reporter_loop {
          reporter_thread = Reporter.instance.start!(Config.instance, collectors)
          reporter_thread.join
        }
      end

      def stub_reporter_loop
        Reporter.instance.stub(:loop, ->(&blk) { blk.call }) {
          Reporter.instance.stub(:sleep, true) {
            yield
          }
        }
      end

      it "sends a report with collected metrics" do
        metrics_collector = TestWebMetricsCollector.new
        metrics = metrics_collector.collect

        expected_body = Report.new(Judoscale.adapters, Config.instance, metrics).as_json
        stub = stub_request(:post, "http://example.com/api/test-token/adapter/v1/metrics")
          .with(body: JSON.generate(expected_body))

        run_reporter_start_thread(collectors: [metrics_collector])

        assert_requested stub
      end

      it "logs exceptions when reporting collected information" do
        Reporter.instance.stub(:report!, ->(*) { raise "REPORT BOOM!" }) {
          run_reporter_start_thread
        }

        _(log_string).must_include "Reporter error: #<RuntimeError: REPORT BOOM!>"
        _(log_string).must_include "lib/judoscale/reporter.rb"
      end

      it "logs exceptions when collecting information" do
        metrics_collector = TestWebMetricsCollector.new

        metrics_collector.stub(:collect, ->(*) { raise "ADAPTER BOOM!" }) {
          run_reporter_start_thread(collectors: [metrics_collector])
        }

        _(log_string).must_include "Reporter error: #<RuntimeError: ADAPTER BOOM!>"
        _(log_string).must_include "lib/judoscale/reporter.rb"
      end

      it "does not run the reporter thread when the API url is not configured" do
        Judoscale.configure { |config| config.api_base_url = nil }

        Thread.stub(:new, ->(*) { raise "SHOULD NOT BE CALLED" }) {
          Reporter.instance.start!(Config.instance, [TestWebMetricsCollector.new])
        }

        _(log_string).must_include "Reporter not started: JUDOSCALE_URL is not set"
      end

      it "does not run the reporter thread when there are no metrics collectors" do
        Thread.stub(:new, ->(*) { raise "SHOULD NOT BE CALLED" }) {
          Reporter.instance.start!(Config.instance, [])
        }

        _(log_string).must_include "Reporter not started: no metrics need to be collected on this dyno"
      end

      it "logs when the reporter starts successfully" do
        stub_request(:post, "http://example.com/api/test-token/adapter/v1/metrics")
        run_reporter_start_thread

        _(log_string).must_include "Reporter starting, will report every 10 seconds or so. Adapters: [judoscale-ruby]"
      end
    end

    describe "#report!" do
      it "reports collected metrics to the API" do
        metrics = [
          Metric.new(:qt, 11, Time.at(1_000_000_001)), # web metric
          Metric.new(:qt, 22, Time.at(1_000_000_002), "high") # worker metric
        ]

        expected_body = Report.new(Judoscale.adapters, Config.instance, metrics).as_json
        stub = stub_request(:post, "http://example.com/api/test-token/adapter/v1/metrics")
          .with(body: JSON.generate(expected_body))

        Reporter.instance.send :report!, Config.instance, metrics

        assert_requested stub
      end

      it "logs reporter failures" do
        stub_request(:post, %r{http://example.com/api/test-token/adapter/v1/metrics})
          .to_return(body: "oops", status: 503)
{'fraction_non_alphanumeric': 0.09007033199727747, 'fraction_numerical': 0.0064281932995538076, 'mean_word_length': 2.705239562902774, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1769327', 'n_tokens_mistral': 3888, 'n_tokens_neox': 3744, 'n_words': 885}