Repository navigation
Expand file tree
/
Copy pathTag.php
More file actions
171 lines (149 loc) · 5.65 KB
/
Copy pathTag.php
File metadata and controls
171 lines (149 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<?php
declare(strict_types=1);
namespace DrevOps\BehatSteps\Extension;
use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested;
use Behat\Behat\Hook\Scope\ScenarioScope;
use Behat\Gherkin\Node\TaggedNodeInterface;
/**
* Reads scenario and feature tags in a form that does not vary by Behat major.
*
* Behat 3 strips the '@' from a tag by default and Behat 4 keeps it, while
* 'TaggedNodeInterface::hasTag()' compares strictly. Every tag this library
* reads goes through here, so a tag matches on both majors.
*
* 'has()', 'values()' and 'valueStates()' take a subject. For a scenario
* scope or event they read the scenario together with its feature, and for a
* node they read that node alone.
*
* A parametrized tag reads '@<name>:<value>', and a '!' before the value
* switches it off, as in '@module:!help'.
*/
final class Tag {
/**
* The tag that runs a scenario in the Mink JavaScript session.
*/
public const string JAVASCRIPT = 'javascript';
/**
* Separates the name of a parametrized tag from its value.
*/
public const string SEPARATOR = ':';
/**
* Switches off the value it precedes, as in '@module:!help'.
*/
public const string NEGATION = '!';
/**
* Strips the leading '@' from each tag.
*
* @param array<array-key, string> $tags
* Tags as the parser produced them.
*
* @return array<int, string>
* Tags without a leading '@'.
*/
public static function normalize(array $tags): array {
return array_values(array_map(
static fn(string $tag): string => str_starts_with($tag, '@') ? substr($tag, 1) : $tag,
$tags
));
}
/**
* Collects the tags of a single node.
*
* @param \Behat\Gherkin\Node\TaggedNodeInterface $node
* The feature or scenario to read.
*
* @return array<int, string>
* The node's tags, each without a leading '@'.
*/
public static function on(TaggedNodeInterface $node): array {
return self::normalize($node->getTags());
}
/**
* Checks whether a subject carries a tag.
*
* @param \Behat\Gherkin\Node\TaggedNodeInterface|\Behat\Behat\Hook\Scope\ScenarioScope|\Behat\Behat\EventDispatcher\Event\BeforeScenarioTested $subject
* A scenario scope or event, to read the scenario and its feature, or a
* single node.
* @param string $name
* The tag name, without a leading '@'.
*
* @return bool
* TRUE when the subject carries the tag.
*/
public static function has(TaggedNodeInterface|ScenarioScope|BeforeScenarioTested $subject, string $name): bool {
$tags = $subject instanceof TaggedNodeInterface ? self::on($subject) : self::all($subject);
return in_array($name, $tags, TRUE);
}
/**
* Collects the tags of a scenario and the feature that holds it.
*
* @param \Behat\Behat\Hook\Scope\ScenarioScope|\Behat\Behat\EventDispatcher\Event\BeforeScenarioTested $subject
* The scenario scope a hook received, or the event a listener received.
*
* @return array<int, string>
* Feature tags followed by scenario tags, each without a leading '@'.
*/
public static function all(ScenarioScope|BeforeScenarioTested $subject): array {
$scenario = $subject->getScenario();
$tags = $subject->getFeature()->getTags();
if ($scenario instanceof TaggedNodeInterface) {
$tags = array_merge($tags, $scenario->getTags());
}
return self::normalize($tags);
}
/**
* Collects the values of a parametrized tag.
*
* '@watchdog:php @watchdog:cron' gives 'php' and 'cron'. A value keeps any
* separator of its own, and a tag with nothing after the separator names no
* value.
*
* @param \Behat\Gherkin\Node\TaggedNodeInterface|\Behat\Behat\Hook\Scope\ScenarioScope|\Behat\Behat\EventDispatcher\Event\BeforeScenarioTested $subject
* A scenario scope or event, to read the scenario and its feature, or a
* single node.
* @param string $name
* The tag name, without a leading '@' or the separator.
*
* @return array<int, string>
* 1 value per tag, feature tags first, each in the order written.
*/
public static function values(TaggedNodeInterface|ScenarioScope|BeforeScenarioTested $subject, string $name): array {
$tags = $subject instanceof TaggedNodeInterface ? self::on($subject) : self::all($subject);
$prefix = $name . self::SEPARATOR;
$values = [];
foreach ($tags as $tag) {
if (str_starts_with($tag, $prefix) && strlen($tag) > strlen($prefix)) {
$values[] = substr($tag, strlen($prefix));
}
}
return $values;
}
/**
* Resolves each value of a parametrized tag into an on/off state.
*
* '@module:help' switches 'help' on and '@module:!help' switches it off. A
* later tag for a value replaces an earlier one, so a scenario tag
* overrides a feature tag.
*
* @param \Behat\Gherkin\Node\TaggedNodeInterface|\Behat\Behat\Hook\Scope\ScenarioScope|\Behat\Behat\EventDispatcher\Event\BeforeScenarioTested $subject
* A scenario scope or event, to read the scenario and its feature, or a
* single node.
* @param string $name
* The tag name, without a leading '@' or the separator.
*
* @return array<string, bool>
* TRUE for a value switched on and FALSE for one switched off, keyed by
* value in the order each was first named.
*/
public static function valueStates(TaggedNodeInterface|ScenarioScope|BeforeScenarioTested $subject, string $name): array {
$states = [];
foreach (self::values($subject, $name) as $value) {
$is_on = !str_starts_with($value, self::NEGATION);
$key = $is_on ? $value : substr($value, strlen(self::NEGATION));
if ($key !== '') {
$states[$key] = $is_on;
}
}
return $states;
}
}