Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
ResponseGenerator
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
6 / 6
19
100.00% covered (success)
100.00%
1 / 1
 prepareActions
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 transformField
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 transform
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 computeDuration
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getLastActionTimestamp
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getActionResponse
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2020  (original work) Open Assessment Technologies SA;
19 *
20 * @author Oleksandr Zagovorychev <zagovorichev@gmail.com>
21 */
22
23declare(strict_types=1);
24
25namespace oat\taoQtiTest\models\runner\synchronisation\synchronisationService;
26
27use common_Exception;
28use common_exception_InconsistentData;
29use common_Logger;
30use oat\oatbox\service\ConfigurableService;
31use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
32use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction;
33use ResolverException;
34
35class ResponseGenerator extends ConfigurableService
36{
37    /**
38     * Typical amount of time added on TimePoints to avoid timestamp collisions.
39     * This value will be used to adjust intervals between moves in the synced time line.
40     */
41    private const TIMEPOINT_INTERVAL = .001;
42
43    /**
44     * @param array $data
45     * @param array $availableActions
46     * @return array
47     */
48    public function prepareActions(array $data, array $availableActions): array
49    {
50        /** @var TestRunnerActionResolver $resolver */
51        $resolver = $this->getServiceLocator()->get(TestRunnerActionResolver::class);
52
53        $actions = [];
54        foreach ($data as $entry) {
55            try {
56                $actions[] = $resolver->resolve($entry, $availableActions);
57            } catch (common_exception_InconsistentData | ResolverException $e) {
58                $responseAction = $entry;
59                $responseAction['error'] = $e->getMessage();
60                $responseAction['success'] = false;
61                $actions[] = $this->transform($responseAction);
62            }
63        }
64
65        // ensure the actions are in chronological order
66        usort($actions, static function ($a, $b) {
67            $timeA = $a instanceof TestRunnerAction ? $a->getTimestamp() : 0;
68            $timeB = $b instanceof TestRunnerAction ? $b->getTimestamp() : 0;
69            return $timeA - $timeB;
70        });
71
72        return $actions;
73    }
74
75    private function transformField(string $fromFieldName, string $toFieldName, array $data): array
76    {
77        if (array_key_exists($fromFieldName, $data)) {
78            $data[$toFieldName] = $data[$fromFieldName];
79            unset($data[$fromFieldName]);
80        }
81        return $data;
82    }
83
84    private function transform($data)
85    {
86        if (is_array($data)) {
87            $data = $this->transformField('parameters', 'requestParameters', $data);
88            $data = $this->transformField('action', 'name', $data);
89        }
90        return $data;
91    }
92
93    /**
94     * @param array $actions
95     * @return float
96     */
97    private function computeDuration(array $actions): float
98    {
99        $duration = 0;
100        foreach ($actions as $action) {
101            if ($action instanceof TestRunnerAction && $action->hasRequestParameter('itemDuration')) {
102                $duration += $action->getRequestParameter('itemDuration') + self::TIMEPOINT_INTERVAL;
103            }
104        }
105        return $duration;
106    }
107
108    /**
109     * determine the start timestamp of the actions:
110     * - check if the total duration of actions to sync is comprised within
111     *   the elapsed time since the last TimePoint.
112     * - otherwise compute the start timestamp from now minus the duration
113     *   (caution! this could introduce inconsistency in the TimeLine as the ranges could be interlaced)
114     *
115     * @param array $actions
116     * @param QtiRunnerServiceContext $serviceContext
117     * @param float $timeNow
118     * @return float
119     */
120    public function getLastActionTimestamp(
121        array $actions,
122        QtiRunnerServiceContext $serviceContext,
123        float $timeNow
124    ): float {
125        $lastRegisteredTimestamp = (float) $serviceContext->getTestSession()->getTimer()->getLastRegisteredTimestamp();
126        $actionsDuration = $this->computeDuration($actions);
127        $elapsed = $timeNow - $lastRegisteredTimestamp;
128        if ($actionsDuration > $elapsed) {
129            common_Logger::t(
130                'Ignoring the last timestamp to take into account the actual duration to sync. Could introduce '
131                    . 'TimeLine inconsistency!'
132            );
133            $lastRegisteredTimestamp = $timeNow - $actionsDuration;
134        }
135
136        return $lastRegisteredTimestamp;
137    }
138
139    /**
140     * @param TestRunnerAction $action
141     * @param float $now
142     * @param float $last
143     * @param QtiRunnerServiceContext $serviceContext
144     * @return array
145     */
146    public function getActionResponse(
147        TestRunnerAction $action,
148        float $now,
149        float &$last,
150        QtiRunnerServiceContext $serviceContext
151    ): array {
152        try {
153            $serviceContext->setSyncingMode($action->getRequestParameter('offline'));
154            if ($action->hasRequestParameter('itemDuration') && $serviceContext->isSyncingMode()) {
155                $last += $action->getRequestParameter('itemDuration') + self::TIMEPOINT_INTERVAL;
156                $action->setTime($last);
157            } else {
158                $action->setTime($now);
159            }
160
161            $action->setServiceContext($serviceContext);
162            $actionResponse = $action->process();
163        } catch (common_Exception $e) {
164            $actionResponse = ['error' => $e->getMessage()];
165            $actionResponse['success'] = false;
166        }
167
168        $actionResponse['name'] = $action->getName();
169        $actionResponse['timestamp'] = $action->getTimeStamp();
170        $actionResponse['requestParameters'] = $action->getRequestParameters();
171
172        return $actionResponse;
173    }
174}