diff --git a/plugins/context_condition_context.inc b/plugins/context_condition_context.inc index aca0e6a..2d077e0 100644 --- a/plugins/context_condition_context.inc +++ b/plugins/context_condition_context.inc @@ -5,61 +5,65 @@ */ class context_condition_context extends context_condition_path { function execute() { - if ($this->condition_used()) { - $active_contexts = array_keys(context_active_contexts()); - foreach ($this->get_contexts() as $context) { - if (!in_array($context->name, $active_contexts, TRUE) && $values = $this->fetch_from_context($context, 'values')) { - // Always check against the active contexts. - if ($this->match(array_keys(context_active_contexts()), $values)) { - $this->condition_met($context); - } - } - } - // If the list of active contexts has changed, we need to recurse. - if ($active_contexts != array_keys(context_active_contexts())) { - $this->execute(); + if (!$this->condition_used()) { + return; + } + + // Contexts that were triggered by other conditions. + $activeOld = context_active_contexts(); + + // Get a list of contexts that may be activated by this condition. + $contexts = array(); + // Prepare context values too. + $activateable = array(); + foreach ($this->get_contexts() as $context) { + if (!isset($activeOld[$context->name]) && ($values = $this->fetch_from_context($context, 'values'))) { + $contexts[$context->name] = $context; + $activateable[$context->name] = $values; } } + $activeOld = array_keys($activeOld); + $activeNew = $this->calculateActiveContexts($activeOld, $activateable); + + // Trigger all necessary contexts. + foreach ($activeNew as $name) { + $this->condition_met($contexts[$name]); + } } /** - * Retrieve all context conditions. + * Calculate the list of contexts that we need to trigger. * - * This method is slightly adapted to context_condition::get_contexts() in - * order to ensure that a context that is used as condition in another context - * gets handled before. + * @param array $activeOld + * machine names of contexts that are already active + * @param array $activateable + * associative array of condition values keyed by context name. + * listing all contexts that use this condition. + * @return array + * machine names of all contexts that meet their conditions. */ - function get_contexts($value = NULL) { - $map = context_condition_map(); - $map = isset($map[$this->plugin]) ? $map[$this->plugin] : array(); + protected function calculateActiveContexts($activeOld, $activateable) { + // Keep a list of already visited states. + $knownStates = array(); + $activeNew = array(); + $knownKey = md5(implode('|', $activeNew)); - $contexts = array(); + // Keep going until we hit an already visited state. This means either + // - We found a stable state. + // - We found a circle. + while (!isset($knownStates[$knownKey])) { + $knownStates[$knownKey] = 1; - // Add the contexts that are needed for conditions in the other contexts - // first. Start with the negated ones first, as we can not unset a met - // condition afterwards. - krsort($map); - foreach ($map as $key => $submap) { - // Negated context conditions start with a "~". - if (substr($key, 0, 1) == "~") { - $key = substr($key, 1); - } - if (!isset($contexts[$key])) { - $context = context_load($key); - // Check if context exists. This will fail for wildcards. - if ($context) { - $contexts[$context->name] = $context; + $activeStep = array_merge($activeOld, $activeNew); + $activeNew = array(); + foreach ($activateable as $name => $values) { + if ($this->match($activeStep, $values)) { + $activeNew[] = $name; } } + $knownKey = md5(implode('|', $activeNew)); } - foreach ($map as $key => $submap) { - foreach ($submap as $name) { - if (!isset($contexts[$name])) { - $context = context_load($name); - $contexts[$context->name] = $context; - } - } - } - return $contexts; + + return $activeNew; } } diff --git a/tests/context.conditions.test b/tests/context.conditions.test index e2cee73..8dc108b 100644 --- a/tests/context.conditions.test +++ b/tests/context.conditions.test @@ -324,30 +324,77 @@ class ContextConditionContextTest extends DrupalWebTestCase { $this->drupalLogin($admin_user); } - function test() { + protected function createContext($name, $deps) { ctools_include('export'); $context = ctools_export_new_object('context'); - $context->name = 'testcontext'; - $context->conditions = array('path' => array('values' => array('admin'))); + $context->name = $name; + if ($deps) { + $context->conditions = array('context' => array('values' => $deps)); + } + else { + $context->conditions = array('path' => array('values' => array('admin'))); + } $context->reactions = array('debug' => array('debug' => TRUE)); $saved = context_save($context); - $this->assertTrue($saved, "Context 'testcontext' saved."); - - $subcontext = ctools_export_new_object('context'); - $subcontext->name = 'subcontext'; - $subcontext->conditions = array('context' => array('values' => array('testcontext'))); - $subcontext->reactions = array('debug' => array('debug' => TRUE)); - $saved = context_save($subcontext); - $this->assertTrue($saved, "Context 'subcontext' saved."); + $this->assertTrue($saved, "Context '$name' saved."); + return $saved; + } + protected function activeAssertions($active, $noactive = array()) { $this->drupalGet('admin'); - $this->assertText('Active context: testcontext'); - $this->assertText('Active context: subcontext'); - - // Cleanup - context_delete($context); + foreach ($active as $name) { + $this->assertText("Active context: $name"); + } + foreach ($noactive as $name) { + $this->assertNoText("Active context: $name"); + } + } - // @TODO: Test exclusion + /** + * Test a simple dependency. + * + * testcontext: is active + * subcontext: depends on testcontext + */ + function test() { + $this->createContext('testcontext', array()); + $this->createContext('subcontext', array('testcontext')); + $this->activeAssertions(array('testcontext', 'subcontext')); + } + + /** + * Test a complex dependency tree with inclusions and exclusions. + * + * a: is active + * b: depends on a + * c: depends on ~b + * d: depends on ~c and b + * + * correct result: a, b and d are active. + */ + function testExclusion() { + $this->createContext('a', array()); + $this->createContext('b', array('a')); + $this->createContext('c', array('~b')); + $this->createContext('d', array('~c', 'b')); + $this->activeAssertions(array('a', 'b', 'd'), array('c')); + } + + /** + * Test circle breaking. This test will never finish if circle breaking + * doesn't work. + * + * a: ~c + * b: ~a + * c: ~b + * + * Has no stable solution any solution is fine. + */ + function testCircleBreaking() { + $this->createContext('a', array('~c')); + $this->createContext('b', array('~a')); + $this->createContext('c', array('~b')); + $this->activeAssertions(array()); } }