PHP Developers Network

A community of PHP developers offering assistance, advice, discussion, and friendship.
 
Loading
It is currently Wed Sep 19, 2018 6:59 am

All times are UTC - 5 hours




Post new topic Reply to topic  [ 7 posts ] 
Author Message
 Post subject: Artificial System Clocks
PostPosted: Fri Oct 17, 2008 9:44 am 
Offline
Forum Newbie

Joined: Fri Oct 17, 2008 9:31 am
Posts: 19
Hello - I would like to introduce a testing pattern called an Artificial System Clock (ASC).

For a detailed explanation of ASCs, visit http://www.timechannels.com

In a language like Java, you implement this by making a class that has a method on it, perhaps called getSystemDate(). This method looks at some kind of global, static variable for the current "state" of the ASC. By default, it's Off, and in the Off state the getSystemDate() method returns the current, real date-time. But if you set the global state variable to On, and provide a specific date-time value, then the getSystemDate() method returns that specific date-time.

You then go through your code base, and everywhere that you are obtaining the date-time directly, you replace it with a call to this getSystemDate() method.

Once you do this, you have an ASC. You have effectively tricked your whole application into thinking that it is a different date-time, without having to alter the server's clock, and this is enormously valuable for testing purposes.

My question to this community is whether this concept could be implemented in php. Does php support the notion of "static" variables? Or is there a way to declare global variables that are shared across the entire codebase of a php application?

I realize this is a very basic question, but I was hoping to solicit some help from php-fluent developers to explore developing the artificial system clock for php applications.

Thanks,

John


Top
 Profile  
 
PostPosted: Fri Oct 17, 2008 8:14 pm 
Offline
Forum Commoner

Joined: Sat Jul 12, 2003 10:31 pm
Posts: 80
Location: London
Hi...

This is also known to us as "mock the clock".

I find I use it more when testing database code. For example in MySQL there is a now() function. We never call it directly, but have written our own time_now() function. This chains straight through except when testing, when it's routed via a single row table. Allows us to write tests like this...
Syntax: [ Download ] [ Hide ]
 
class AccountTest extends UnitTestCase {
    ...
    function testCanReadBackMultipleAccountNotes() {
        $account = $this->accounts()->createAccount();
        $this->nowJuly(1, 2008);
        $this->accounts()->addNote($account, 'A bid dodgy');
        $this->nowJuly(2, 2008);
        $this->accounts()->addNote($account, 'Not so bad');
        $this->assertEqual(
                $this->accounts()->getNotes($account),
                array('1/7/2008' => 'A bid dodgy', '2/7/2008' => 'Not so bad'));
    }
 
    function accounts() {
        return new Accounts();   // Methods map to stored procs.
    }
}
 


I like the idea of the distributed web service clock though. Very neat idea.

yours, Marcus


Top
 Profile  
 
PostPosted: Sat Oct 18, 2008 8:14 am 
Offline
Forum Newbie

Joined: Fri Oct 17, 2008 9:31 am
Posts: 19
Thanks Marcus. Yes, the first stumbling block my team hit when implementing this was the database. We ended up basically putting the ASC date-time value in a table in the DB, and did something very similar to what you illustrated.

I then also pointed the application (Java) code to look in that same table, and so then we had the whole system on the same page. I was amazed how quickly the team got addicted to it - there's really no way they would have gotten all that testing done without it.

But the artificial system clock (ASC) concept was summarily limited to our system: when we went to test with other upstream/downstream systems, they would sometimes have difficulty dealing with data that was stale-dated, or future-dated. If they could all implement that same ASC concept, and pointed it to the same value from a 3rd-party service, then the testing coordination would have been spectacular.

Alas, when I left the large financial organization last February, my next step was going to be to rally other teams to implement the same ASC concept. But let's just say that management there had plenty of other big issues to deal with, and the organization has now been usurped by the government.

So I started http://www.timechannels.com, a free web-service to publish ASCs with. I have the code written for Java, which is available to download and use now, and a friend has given me the .Net client software, which I will make available in the next few days.

I'm now looking for help to complete the php client software, if it's even possible.

(If anyone wishes to contact me privately, please feel free to reach me via email: john@timechannels.com)

Cheers,

John


Top
 Profile  
 
PostPosted: Mon Oct 20, 2008 4:50 am 
Offline
DevNet Master
User avatar

Joined: Mon Sep 19, 2005 6:24 am
Posts: 3587
Location: London
Quote:
In a language like Java, you implement this by making a class that has a method on it, perhaps called getSystemDate(). This method looks at some kind of global, static variable for the current "state" of the ASC. By default, it's Off, and in the Off state the getSystemDate() method returns the current, real date-time. But if you set the global state variable to On, and provide a specific date-time value, then the getSystemDate() method returns that specific date-time.
I see no reason why the same cannot be done in PHP :)

Though instead of using a global, just use a stub or perhaps even a mock.


Top
 Profile  
 
PostPosted: Mon Oct 20, 2008 11:36 am 
Offline
Forum Newbie

Joined: Fri Oct 17, 2008 9:31 am
Posts: 19
Thank you Jenk. You are right, a global variable for the clock value would partially defeat the purpose, but also would potentially enable "bad programming behavior". Local static vars. on a class are pretty much globally accessible, unless you make them private, and then they can only be modified through the api implemented by the class. I just don't know if php supports these OO concepts or not.

Also, is it possible to invoke a web-service in php?

John


Top
 Profile  
 
PostPosted: Sat Nov 08, 2008 10:27 am 
Offline
Forum Newbie

Joined: Fri Oct 17, 2008 9:31 am
Posts: 19
Just wanted to update this topic with some solutions, which are now available to download and use from http://www.timechannels.com

Since very early versions of php were not object-oriented, I made two versions of this: one for OO, and one that I'm calling "Legacy", which just uses global variables instead of static class variables.

Here's the OO version:

Syntax: [ Download ] [ Hide ]
<?php
 
/*
 * This is the TimeChannels Artificial System Clock implementation class for PHP.
 * Everywhere in your application codebase that obtains the date/time directly, replace with a call
 * to the getSystemDate() function in this class.
 *
 * For simple unit-testing, you can call setToStaticTestState() and pass it a specific date-time value.
 *
 * For system, integration, acpt testing, call setToTestState() and pass it a specific channel id you have setup in
 * the TimeChannels.com control panel.
 *
 * Return to normal time by calling setToProductionState()
 */

 
class TimeChannels {
  private static $tcURL = "http://www.timechannels.com/GetChannel";
  private static $tcURLms = "http://www.timechannels.com/GetChannelMS";
  private static $tcAscState = "TimeChannels.state.Production";
  private static $tcChannel = "0";
  private static $tcAscDateTime = null;
  private static $tcServiceURL = "http://www.timechannels.com/GetChannel";
  private static $tcMillisecondMode = "false";
 
  public static function setToProductionState() {
    self::$tcAscState = "TimeChannels.state.Production";
  }
 
  public static function setToStaticTestState($aDateTimeValue) {
    self::$tcAscState = "TimeChannels.state.TestDate";
    self::$tcAscDateTime = $aDateTimeValue;
  }
 
  public static function setToTestState($aChannel) {
    self::$tcAscState = "TimeChannels.state.TestChannel";
    self::$tcChannel = $aChannel;
  }
 
  public static function setToMillisecondMode() {
    self::$tcMillisecondMode = "true";
    self::$tcServiceURL = self::$tcURLms;
  }
 
  public static function setToStandardMode() {
    self::$tcMillisecondMode = "false";
    self::$tcServiceURL = self::$tcURL;
  }
 
  public static function getSystemDate() {
    if (self::$tcAscState == "TimeChannels.state.Production") return mktime();
 
    if (self::$tcAscState == "TimeChannels.state.TestDate") return self::$tcAscDateTime;
 
    if (self::$tcAscState == "TimeChannels.state.TestChannel") return self::getChannelFromTimeChannelsService(self::$tcChannel);
 
    return mktime();  
  }
 
  private static function getHTTPContent( $url ) {
    $options = array( 'http' => array(
        'user_agent'    => 'timechannelsclient',    // who am i
        'max_redirects' => 10,          // stop after 10 redirects
        'timeout'       => 120,         // timeout on response
    ) );
    $context = stream_context_create($options);
    $page = @file_get_contents($url, false, $context);
 
    $result = array();
    if ($page != false)
      $result['content'] = $page;
    else if (!isset($http_response_header))
      return null;    // Bad url, timeout
 
    $result['header'] = $http_response_header;
 
    $nLines = count($http_response_header);
    for ($i = $nLines-1; $i >= 0; $i--) {
      $line = $http_response_header[$i];
      if (strncasecmp("HTTP", $line, 4) == 0) {
        $response = explode(' ', $line);
        $result['http_code'] = $response[1];
        break;
      }
    }
    return $result;
  }
 
  private static function getChannelFromTimeChannelsService($aChannel) {
 
    $url = self::$tcServiceURL . "?channel=" . $aChannel;
    $result = self::getHTTPContent($url);
 
    if ($result == null) return mktime();
    if ($result['http_code'] != 200) return mktime();
 
    $channelValue = $result['content'];
 
    if (self::$tcMillisecondMode == "true") {
      $retVal = (1 * $channelValue) / 1000;
    }
    else {
      $year = 1 * substr($channelValue, 0, 4);
        //echo "year: $year <br>";
      $month = 1 * substr($channelValue, 5, 2);
        //echo "month: $month <br>";
      $day = 1* substr($channelValue, 8, 2);
        //echo "day: $day <br>";
      $hour = 1 * substr($channelValue, 11, 2);
        //echo "hour: $hour <br>";
      $minute = 1 * substr($channelValue, 14, 2);
        //echo "minute: $minute <br>";
      $ampm = substr($channelValue, 17, 2);
        //echo "ampm: $ampm <br>";
 
      if ($ampm == "PM") $hour += 12;
 
      $retVal = mktime($hour, $minute, 0, $month, $day, $year);
    }
 
    return $retVal;
  }
 
}
 
?>
 


Here's the sample usage/test code for the above:

Syntax: [ Download ] [ Hide ]
<?php
 
include 'TimeChannels.php';
 
$tab = "&nbsp;&nbsp;";
 
echo "======================================================================<br>";
echo $tab . "&lt;TimeChannelsModernClient&gt;<br>";
 
/////////// --- Don't do anything yet; test that TimeChannels class defaults to Prod. Mode:
echo $tab . $tab . "---> default result: " . date("Y-m-d h:i A", TimeChannels::getSystemDate());
echo "<br>";
 
/////////// --- Now set to Test Mode with a static date:
$staticDate = mktime(9, 15, 0, 3, 13, 1969);
TimeChannels::setToStaticTestState($staticDate);
echo $tab . $tab . "---> static test state result: " . date("Y-m-d h:i A", TimeChannels::getSystemDate());
echo "<br>";
 
/////////// --- Now set to Test Mode with a channel:
TimeChannels::setToTestState("2");
echo $tab . $tab . "---> channel 2 test state result: " . date("Y-m-d h:i A", TimeChannels::getSystemDate());
echo "<br>";
 
/////////// --- Now set to Millisecond Mode:
// Please note:  millisecond timestamps are not supported directly in php, so this call will only be precise
// to the second.
TimeChannels::setToMillisecondMode();
echo $tab . $tab . "---> channel 2 test state result from milliseconds: " . date("Y-m-d h:i:s A", TimeChannels::getSystemDate());
echo "<br>";
 
echo $tab . "&lt;/TimeChannelsClient&gt;<br>";
echo "======================================================================<br>";
 
?>
 


Here is the "Legacy" version of the client:

Syntax: [ Download ] [ Hide ]
<?php
 
/* This is a rudimentary implementation of the TimeChannels Artificial System Clock, intended for use with very old
 * php versions which don't support classes.  PHP 4+ users should use the class-based TimeChannels.php source.
 *
 * Please note the following global vars - do not overwrite these.  If there is a conflict with other global vars in
 * your application, these can be renamed, but of course make sure all references in this file match.
 *
 * Everywhere in your application codebase that obtains the date/time directly, replace with a call
 * to the getSystemDate() function in this class.
 *
 * For simple unit-testing, you can call setToStaticTestState() and pass it a specific date-time value.
 *
 * For system, integration, acpt testing, call setToTestState() and pass it a specific channel id you have setup in
 * the TimeChannels.com control panel.
 *
 * Return to normal time by calling setToProductionState()
 */

 
$tcURL = "http://www.timechannels.com/GetChannel";
$tcURLms = "http://www.timechannels.com/GetChannelMS";
$tcAscState = "TimeChannels.state.Production";
$tcChannel = "0";
$tcAscDateTime = null;
$tcServiceURL = $tcURL;
$tcMillisecondMode = "false";
 
 
function setToProductionState() {
  global $tcAscState;
  $tcAscState = "TimeChannels.state.Production";
}
 
 
function setToStaticTestState($aDateTimeValue) {
  global $tcAscState, $tcAscDateTime;
  $tcAscState = "TimeChannels.state.TestDate";
  $tcAscDateTime = $aDateTimeValue;
}
 
 
function setToTestState($aChannel) {
  global $tcAscState, $tcChannel;
  $tcAscState = "TimeChannels.state.TestChannel";
  $tcChannel = $aChannel;
}
 
 
function setToMillisecondMode() {
  global $tcMillisecondMode, $tcServiceURL, $tcURLms;
  $tcMillisecondMode = "true";
  $tcServiceURL = $tcURLms;
}
 
 
function setToStandardMode() {
  global $tcMillisecondMode, $tcServiceURL, $tcURL;
  $tcMillisecondMode = "false";
  $tcServiceURL = $tcURL;
}
 
 
function getSystemDate() {
  global $tcAscState, $tcAscDateTime, $tcChannel;
 
  if ($tcAscState == "TimeChannels.state.Production") return mktime();
 
  if ($tcAscState == "TimeChannels.state.TestDate") return $tcAscDateTime;
 
  if ($tcAscState == "TimeChannels.state.TestChannel") return getChannelFromTimeChannelsService($tcChannel);
 
  return mktime();  
}
 
 
function getHTTPContent( $url ) {
  $options = array( 'http' => array(
      'user_agent'    => 'spider',    // who am i
      'max_redirects' => 10,          // stop after 10 redirects
      'timeout'       => 120,         // timeout on response
  ) );
  $context = stream_context_create($options);
  $page = @file_get_contents($url, false, $context);
 
  $result = array();
  if ($page != false)
    $result['content'] = $page;
  else if (!isset($http_response_header))
    return null;    // Bad url, timeout
 
  $result['header'] = $http_response_header;
 
  $nLines = count($http_response_header);
  for ($i = $nLines-1; $i >= 0; $i--) {
    $line = $http_response_header[$i];
    if (strncasecmp("HTTP", $line, 4) == 0) {
      $response = explode(' ', $line);
      $result['http_code'] = $response[1];
      break;
    }
  }
  return $result;
}
 
 
function getChannelFromTimeChannelsService($aChannel) {
  global $tcServiceURL, $tcMillisecondMode;
 
  $url = $tcServiceURL . "?channel=" . $aChannel;
  $result = getHTTPContent($url);
 
  if ($result == null) return mktime();
  if ($result['http_code'] != 200) return mktime();
 
  $channelValue = $result['content'];
 
  if ($tcMillisecondMode == "true") {
    $retVal = (1 * $channelValue) / 1000;
  }
  else {
    $year = 1 * substr($channelValue, 0, 4);
      //echo "year: $year <br>";
    $month = 1 * substr($channelValue, 5, 2);
      //echo "month: $month <br>";
    $day = 1* substr($channelValue, 8, 2);
      //echo "day: $day <br>";
    $hour = 1 * substr($channelValue, 11, 2);
      //echo "hour: $hour <br>";
    $minute = 1 * substr($channelValue, 14, 2);
      //echo "minute: $minute <br>";
    $ampm = substr($channelValue, 17, 2);
      //echo "ampm: $ampm <br>";
 
    if ($ampm == "PM") $hour += 12;
 
    $retVal = mktime($hour, $minute, 0, $month, $day, $year);
  }
 
  return $retVal;
 
}
 
 
?>
 


And here's the sample usage/testing code for the "Legacy" version:

Syntax: [ Download ] [ Hide ]
<?php
 
include 'timechannelsLegacy.php';
 
$tab = "&nbsp;&nbsp;";
 
echo "======================================================================<br>";
echo $tab . "&lt;TimeChannelsClient&gt;<br>";
 
/////////// --- Don't do anything yet; test that TimeChannels class defaults to Prod. Mode:
echo $tab . $tab . "---> default result: " . date("Y-m-d h:i A", getSystemDate());
echo "<br>";
 
/////////// --- Now set to Test Mode with a static date:
$staticDate = mktime(9, 15, 0, 3, 13, 1969);
setToStaticTestState($staticDate);
echo $tab . $tab . "---> static test state result: " . date("Y-m-d h:i A", getSystemDate());
echo "<br>";
 
/////////// --- Now set to Test Mode with a channel:
setToTestState("2");
echo $tab . $tab . "---> channel 2 test state result: " . date("Y-m-d h:i A", getSystemDate());
echo "<br>";
 
/////////// --- Now set to Millisecond Mode:
// Please note:  millisecond timestamps are not supported directly in php, so this call will only be precise
// to the second.
setToMillisecondMode();
echo $tab . $tab . "---> channel 2 test state result from milliseconds: " . date("Y-m-d h:i:s A", getSystemDate());
echo "<br>";
 
echo $tab . "&lt;/TimeChannelsClient&gt;<br>";
echo "======================================================================<br>";
 
?>
 


I hope you will all consider using this when developing projects (PHP or otherwise). Converting your date-time calls to instead use the timechannels class method ( getSystemDate() ) is really easy, and there's not much risk to it either, as the default state is to Production mode, so it's only going to reflect the regular (real) date-time anyway, until you turn the ASC on.

When I first implemented this ASC concept, it was on a large project that was about a year-and-a-half and three releases into development, with over a million lines of Java code. Still, it only took myself and another developer about two days to go through the entire codebase with our initial pass and replace date-time calls. We of course missed several spots, but these were of course caught in various levels of routine testing.

At first blush, I'd say that this should be much easier in php, because it looks like in php there's only one way to deal with date-times, yes? It's harder in Java because Java provides multiple, convoluted ways of dealing with date-times, and these often conflict with each other, so each occurrence must be carefully studied to ensure that the ASC is going to return a value that is compatible with the receiving code.

Anyway, hope this helps, and I would love to hear your feedback/suggestions.

Cheers,

John


Last edited by Benjamin on Thu Jun 11, 2009 7:33 pm, edited 1 time in total.
Changed code type from text to php.


Top
 Profile  
 
PostPosted: Thu Jun 11, 2009 3:00 pm 
Offline
Forum Newbie

Joined: Fri Oct 17, 2008 9:31 am
Posts: 19
Has anyone figure out a better way than the global var's I used to deal with "Legacy" PHP versions?

Just curious...

John


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 7 posts ] 

All times are UTC - 5 hours


Who is online

Users browsing this forum: No registered users and 4 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Jump to:  
Powered by phpBB® Forum Software © phpBB Group