The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Evo::Realm

VERSION

version 0.0195

SYNOPSYS

See the full example at the bottom of this doc "BUILDING REALM"

  package main;
  use Evo;

  {

    package My::Log;
    use Evo '-Comp *; -Realm *';
    sub msg($self, $msg) { say $msg }

    package My::MockLog;
    use Evo '-Comp *';
    sub msg($self, $msg) { say "MOCK" }
  };


  my $default = My::Log::new();
  my $mock    = My::MockLog::new();

  My::Log::realm $mock, sub {
    $default->realm_lord->msg('hello');    # MOCK
  };

  $default->realm_lord->msg('hello');      # hello

FUNCTIONS

realm

Start a new realm (the last argument) with the lord (the first argument).

  my $silent_log = My::Log::new(level => 1);
  My::Log::realm $silent_log, sub { };
  My::Log::realm $silent_log, 'arg1', 'arg2', sub { };

realm_lord

Get the lord of current realm. If we're not in the realm of the module, return the passed argument or die

  my $default = My::Log::new();
  my $lord    = My::Log::realm_lord($default);    # return current lord or $default
  $lord = $default->realm_lord;                   # the same ($default is an instance of My::Log)

  $lord = My::Log::realm_lord();                  # return current lord or die

TESTING

The good example of usage is Evo::Loop. Consider you have following application:

  package My::App;
  use Evo '-Comp *; -Loop *';

  has name  => 'default';
  has delay => 10;

  sub rename_later($self, $name) {
    loop_timer $self->delay, sub { $self->name($name) }
  }

Method rename_later starts a timer, that will eventually change name. We need to test this behaviour: if the timer was fired only once and if the name was changed rightly. We can't test it simple way:

  my $app = My::App::new(name => 'old');
  $app->rename_later('alex');
  is $app->name, 'alex';

That's because the name will be changed only after 10 seconds. Also we can't mock timer, because another tests are running right now and mocking event loop can break them.

If we were using singleton, we would either write slow blocking test with different loop for each one, or wouldn't be able to test this method at all. That's the weakness of "singleton" and "default" patterns

It's time for Evo::Realm pattern. We're could create a different instance of the component (or even mocked one), create a realm and make that instance to be in charge.

Let's write a mocked loop

  package My::MockLoopComp;
  use Evo '-Comp *';
  has stash => sub { {} };
  sub timer($self, $delay, $fn) { $self->stash->{count}++; $fn->() }

And now how to make it a lord:

  my $mock_loop = My::MockLoopComp::new();
  Evo::Loop::Comp::realm $mock_loop, sub {
    $app->rename_later('alex');
  };

We created a realm. In that realm every invocation of "loop_timer" in Evo::Loop will call a mocked version, but won't interfere with other timers, so we can run multiple tests in parallel.

Below is a full example, you can copy-paste-and-run it.

  package main;
  use Evo '-Loop *; Test::More; Time::HiRes time';

  # our app we're going to test
  {

    package My::App;
    use Evo '-Comp *; -Loop *';

    has name  => 'default';
    has delay => 10;

    sub rename_later($self, $name) {
      loop_timer $self->delay, sub { $self->name($name) }
    }

  };

  # Create a mock loop. It executes function blocking and count invocations
  {

    package My::MockLoopComp;
    use Evo '-Comp *';
    has stash => sub { {} };
    sub timer($self, $delay, $fn) { $self->stash->{count}++; $fn->() }
  }


  # simulate multiple tests to show that we don't break global timers
  loop_timer 0.5, sub { say "delay 0.5" };

  # here starts our test asynchroniously
  loop_postpone sub {

    # create mock loop and an instance of our app
    my $app = My::App::new(name => 'old');
    my $mock_loop = My::MockLoopComp::new();

    # make mock loop a lord for this realm:
    Evo::Loop::Comp::realm $mock_loop, sub {
      $app->rename_later('alex');
    };

    is $app->name, 'alex';
    is $mock_loop->stash->{count}, 1;

  };


  loop_start();

  done_testing;

BUILDING REALM

To build component with realm, just import Evo::Realm '*' into the component's package.

    package My::Log;
    use Evo '-Comp *; -Realm *';
    sub msg($self, $msg) { say $msg }

After that you can use it like this:

  my $mock    = My::MockLog::new();
  My::Log::realm $mock, sub {
    $default->realm_lord->msg('hello');    # MOCK
  };

This form isn't convenient: it's lot of typing and you can miss ->realm_lord part by accident. Let's improve our log and make it simple like mylog('hello')

    package My::Lib;
    use Evo '-Export *';
    use constant DEFAULT_LOG => My::Log::new();

    sub mylog : Export { DEFAULT_LOG->realm_lord->msg(@_) }

This library does all boring stuff for us. Now we can import it use My::Lib '*'; and use mylog function.

The full improved example from the SYNOPSYS:

  package main;
  use Evo;

  {

    package My::Log;
    use Evo '-Comp *; -Realm *';
    sub msg($self, $msg) { say $msg }

    package My::MockLog;
    use Evo '-Comp *';
    sub msg($self, $msg) { say "MOCK" }

    package My::Lib;
    use Evo '-Export *';
    use constant DEFAULT_LOG => My::Log::new();

    sub mylog : Export { DEFAULT_LOG->realm_lord->msg(@_) }

  };

  My::Lib->import('*');    # use My::Lib '*'; in real code
  my $mock = My::MockLog::new();

  My::Log::realm $mock, sub {
    mylog('hello');        # MOCK
  };

  mylog('hello');          # hello

AUTHOR

alexbyk.com

COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by alexbyk.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.