clqms-be/tests/unit/Rules/RuleExpressionSyntaxTest.php
root 2bcdf09b55 chore: repo-wide normalization + rules test coverage
Normalize formatting/line endings across configs, controllers, models, tests, and OpenAPI specs.

Update rule expression/rule engine implementation and remove obsolete RuleAction controller/model.

Add unit tests for rule expression syntax and multi-action behavior, and include docs updates.
2026-03-16 07:24:50 +07:00

347 lines
13 KiB
PHP

<?php
namespace Tests\Unit\Rules;
use App\Services\RuleExpressionService;
use CodeIgniter\Test\CIUnitTestCase;
/**
* Tests for Rule DSL syntax - semicolon syntax, multi-actions, operators
*/
class RuleExpressionSyntaxTest extends CIUnitTestCase
{
protected RuleExpressionService $service;
public function setUp(): void
{
parent::setUp();
$this->service = new RuleExpressionService();
}
// ============================================
// SYNTAX TESTS
// ============================================
public function testSemicolonSyntaxBasic()
{
$result = $this->service->compile('if(sex("M"); result_set(0.5); result_set(0.6))');
$this->assertArrayHasKey('conditionExpr', $result);
$this->assertArrayHasKey('valueExpr', $result);
$this->assertArrayHasKey('then', $result);
$this->assertArrayHasKey('else', $result);
$this->assertEquals('patient["Sex"] == "M"', $result['conditionExpr']);
$this->assertEquals('0.5', $result['then'][0]['valueExpr']);
$this->assertEquals('0.6', $result['else'][0]['valueExpr']);
}
public function testTernarySyntaxFallback()
{
// Legacy ternary syntax should still work (fallback)
$result = $this->service->compile('if(sex("M") ? result_set(0.5) : result_set(0.6))');
$this->assertArrayHasKey('conditionExpr', $result);
$this->assertEquals('patient["Sex"] == "M"', $result['conditionExpr']);
}
// ============================================
// MULTI-ACTION TESTS
// ============================================
public function testMultiActionWithColon()
{
$result = $this->service->compile('if(sex("M"); result_set(0.5):test_insert("HBA1C"); nothing)');
$this->assertCount(2, $result['then']);
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
$this->assertEquals(0.5, $result['then'][0]['value']);
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
$this->assertEquals('HBA1C', $result['then'][1]['testCode']);
$this->assertEquals('NO_OP', $result['else'][0]['type']);
}
public function testThreeActions()
{
$result = $this->service->compile('if(priority("S"); result_set("URGENT"):test_insert("STAT_TEST"):comment_insert("Stat order"); nothing)');
$this->assertCount(3, $result['then']);
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
$this->assertEquals('URGENT', $result['then'][0]['value']);
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
$this->assertEquals('STAT_TEST', $result['then'][1]['testCode']);
$this->assertEquals('COMMENT_INSERT', $result['then'][2]['type']);
$this->assertEquals('Stat order', $result['then'][2]['comment']);
}
public function testMultiActionInElse()
{
$result = $this->service->compile('if(sex("M"); result_set(1.0); result_set(0.8):comment_insert("Default"))');
$this->assertCount(1, $result['then']);
$this->assertCount(2, $result['else']);
$this->assertEquals('RESULT_SET', $result['else'][0]['type']);
$this->assertEquals(0.8, $result['else'][0]['value']);
$this->assertEquals('COMMENT_INSERT', $result['else'][1]['type']);
}
// ============================================
// LOGICAL OPERATOR TESTS
// ============================================
public function testAndOperator()
{
$result = $this->service->compile('if(sex("M") && age > 40; result_set(1.2); result_set(1.0))');
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
$this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']);
$this->assertStringContainsString('age > 40', $result['conditionExpr']);
}
public function testOrOperator()
{
$result = $this->service->compile('if(sex("M") || age > 65; result_set(1.0); result_set(0.8))');
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
$this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']);
$this->assertStringContainsString('age > 65', $result['conditionExpr']);
}
public function testCombinedAndOr()
{
$result = $this->service->compile('if((sex("M") && age > 40) || (sex("F") && age > 50); result_set(1.5); result_set(1.0))');
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
}
public function testComplexNestedCondition()
{
$result = $this->service->compile('if(sex("M") && (age > 40 || priority("S")); result_set(1.2); nothing)');
$this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']);
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
}
// ============================================
// ACTION TESTS
// ============================================
public function testNothingAction()
{
$result = $this->service->compile('if(sex("M"); result_set(0.5); nothing)');
$this->assertEquals('NO_OP', $result['else'][0]['type']);
}
public function testNothingActionInThen()
{
$result = $this->service->compile('if(sex("M"); nothing; result_set(0.6))');
$this->assertEquals('NO_OP', $result['then'][0]['type']);
}
public function testInsertAction()
{
$result = $this->service->compile('if(requested("GLU"); test_insert("HBA1C"); nothing)');
$this->assertEquals('TEST_INSERT', $result['then'][0]['type']);
$this->assertEquals('HBA1C', $result['then'][0]['testCode']);
}
public function testAddCommentAction()
{
$result = $this->service->compile('if(sex("M"); comment_insert("Male patient"); nothing)');
$this->assertEquals('COMMENT_INSERT', $result['then'][0]['type']);
$this->assertEquals('Male patient', $result['then'][0]['comment']);
}
// ============================================
// CONDITION TESTS
// ============================================
public function testSexCondition()
{
$result = $this->service->compile('if(sex("F"); result_set(0.7); result_set(1.0))');
$this->assertEquals('patient["Sex"] == "F"', $result['conditionExpr']);
}
public function testPriorityCondition()
{
$result = $this->service->compile('if(priority("S"); result_set("URGENT"); result_set("NORMAL"))');
$this->assertEquals('order["Priority"] == "S"', $result['conditionExpr']);
}
public function testPriorityUrgent()
{
$result = $this->service->compile('if(priority("U"); result_set("CRITICAL"); nothing)');
$this->assertEquals('order["Priority"] == "U"', $result['conditionExpr']);
}
public function testAgeCondition()
{
$result = $this->service->compile('if(age > 18; result_set(1.0); result_set(0.5))');
$this->assertEquals('age > 18', $result['conditionExpr']);
}
public function testAgeGreaterThanEqual()
{
$result = $this->service->compile('if(age >= 18; result_set(1.0); nothing)');
$this->assertEquals('age >= 18', $result['conditionExpr']);
}
public function testAgeLessThan()
{
$result = $this->service->compile('if(age < 65; result_set(1.0); nothing)');
$this->assertEquals('age < 65', $result['conditionExpr']);
}
public function testAgeRange()
{
$result = $this->service->compile('if(age >= 18 && age <= 65; result_set(1.0); nothing)');
$this->assertStringContainsString('age >= 18', $result['conditionExpr']);
$this->assertStringContainsString('age <= 65', $result['conditionExpr']);
}
public function testRequestedCondition()
{
$result = $this->service->compile('if(requested("GLU"); test_insert("HBA1C"); nothing)');
$this->assertStringContainsString('requested', $result['conditionExpr']);
$this->assertStringContainsString('GLU', $result['conditionExpr']);
}
// ============================================
// MULTI-RULE TESTS
// ============================================
public function testParseMultiRule()
{
$expr = 'if(sex("M"); result_set(0.5); nothing), if(age > 65; result_set(1.0); nothing)';
$rules = $this->service->parseMultiRule($expr);
$this->assertCount(2, $rules);
$this->assertStringContainsString('sex("M")', $rules[0]);
$this->assertStringContainsString('age > 65', $rules[1]);
}
public function testParseMultiRuleThreeRules()
{
$expr = 'if(sex("M"); result_set(0.5); nothing), if(age > 65; result_set(1.0); nothing), if(priority("S"); result_set(2.0); nothing)';
$rules = $this->service->parseMultiRule($expr);
$this->assertCount(3, $rules);
}
// ============================================
// ERROR HANDLING TESTS
// ============================================
public function testInvalidSyntaxThrowsException()
{
$this->expectException(\InvalidArgumentException::class);
$this->service->compile('invalid syntax here');
}
public function testUnknownActionThrowsException()
{
$this->expectException(\InvalidArgumentException::class);
$this->service->compile('if(sex("M"); unknown_action(); nothing)');
}
public function testEmptyExpressionReturnsEmptyArray()
{
$result = $this->service->compile('');
$this->assertEmpty($result);
}
// ============================================
// DOCUMENTATION EXAMPLES
// ============================================
public function testExample1SexBasedResult()
{
// Example 1 from docs
$result = $this->service->compile('if(sex("M"); result_set(0.5); result_set(0.6))');
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
$this->assertEquals(0.5, $result['then'][0]['value']);
$this->assertEquals('RESULT_SET', $result['else'][0]['type']);
$this->assertEquals(0.6, $result['else'][0]['value']);
}
public function testExample2ConditionalTestInsertion()
{
// Example 2 from docs
$result = $this->service->compile("if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing)");
$this->assertCount(2, $result['then']);
$this->assertEquals('TEST_INSERT', $result['then'][0]['type']);
$this->assertEquals('HBA1C', $result['then'][0]['testCode']);
$this->assertEquals('INS', $result['then'][1]['testCode']);
}
public function testExample3MultipleConditionsAnd()
{
// Example 3 from docs
$result = $this->service->compile("if(sex('M') && age > 40; result_set(1.2); result_set(1.0))");
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
}
public function testExample4OrCondition()
{
// Example 4 from docs
$result = $this->service->compile("if(sex('M') || age > 65; result_set(1.0); result_set(0.8))");
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
}
public function testExample5CombinedAndOr()
{
// Example 5 from docs
$result = $this->service->compile("if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0))");
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
}
public function testExample6MultipleActions()
{
// Example 6 from docs
$result = $this->service->compile("if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL'))");
$this->assertCount(2, $result['then']);
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
}
public function testExample7ThreeActions()
{
// Example 7 from docs
$result = $this->service->compile("if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing)");
$this->assertCount(3, $result['then']);
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
$this->assertEquals('COMMENT_INSERT', $result['then'][2]['type']);
}
public function testExample8ComplexRule()
{
// Example 8 from docs
$result = $this->service->compile("if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL'))");
$this->assertCount(2, $result['then']);
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
}
}