]> git.basschouten.com Git - openhab-addons.git/blob
f591b2940883e1ae7b1a9a14e4fba627dc1aad51
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.networkupstools.internal;
14
15 import static org.hamcrest.MatcherAssert.assertThat;
16 import static org.hamcrest.Matchers.is;
17 import static org.junit.jupiter.api.Assertions.*;
18
19 import java.io.IOException;
20 import java.net.URISyntaxException;
21 import java.nio.file.Files;
22 import java.nio.file.Path;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.function.Function;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 import org.junit.jupiter.api.Test;
34 import org.openhab.core.library.CoreItemFactory;
35
36 /**
37  * Test class that reads the README.md and matches it with the OH-INF thing channel definitions.
38  *
39  * @author Hilbrand Bouwkamp - Initial contribution
40  */
41 public class NutNameChannelsTest {
42
43     private static final String THING_TYPES_XML = "thing-types.xml";
44     private static final String CHANNELS_XML = "channels.xml";
45
46     private static final int EXPECTED_NUMBER_OF_CHANNELS = 20;
47     private static final int EXPECTED_NUMMBER_OF_CHANNEL_XML_LINES = EXPECTED_NUMBER_OF_CHANNELS * 6;
48
49     // README table is: | Channel Name | Item Type | Unit | Description | Advanced
50     private static final Pattern README_PATTERN = Pattern
51             .compile("^\\|\\s+([\\w\\.]+)\\s+\\|\\s+([:\\w]+)\\s+\\|\\s+([^\\|]+)\\|\\s+([^\\|]+)\\|\\s+([^\\s]+)");
52     private static final Pattern CHANNEL_PATTERN = Pattern.compile("<channel id");
53     private static final Pattern CHANNEL_TYPE_PATTERN = Pattern
54             .compile("(<channel-type|<item-type|<label|<description|<state|</channel-type)");
55
56     private static final String TEMPLATE_CHANNEL_TYPE = "<channel-type id=\"%s\"%s>";
57     private static final String TEMPLATE_ADVANCED = " advanced=\"true\"";
58     private static final String TEMPLATE_ITEM_TYPE = "<item-type>%s</item-type>";
59     private static final String TEMPLATE_LABEL = "<label>%s</label>";
60     private static final String TEMPLATE_DESCRIPTION = "<description>%s</description>";
61     private static final String TEMPLATE_STATE = "<state pattern=\"%s\" readOnly=\"true\"/>";
62     private static final String TEMPLATE_STATE_NO_PATTERN = "<state readOnly=\"true\"/>";
63     private static final String TEMPLATE_STATE_OPTIONS = "<state readOnly=\"true\">";
64     private static final String TEMPLATE_CHANNEL_TYPE_END = "</channel-type>";
65     private static final String TEMPLATE_CHANNEL = "<channel id=\"%s\" typeId=\"%s\"/>";
66
67     private static final String README_IS_ADVANCED = "yes";
68
69     /**
70      * Test if README matches with the channels in the things xml.
71      */
72     @Test
73     public void testReadmeMatchingChannels() {
74         final Map<NutName, String> readMeNutNames = readReadme();
75         final List<String> list = new ArrayList<>();
76
77         for (final Entry<NutName, String> entry : readMeNutNames.entrySet()) {
78             final Matcher matcher = README_PATTERN.matcher(entry.getValue());
79
80             assertNotNull(entry.getKey(), "Could not find NutName in readme for : " + entry.getValue());
81             if (matcher.find()) {
82                 list.add(String.format(TEMPLATE_CHANNEL, entry.getKey().getChannelId(),
83                         nutNameToChannelType(entry.getKey())));
84             } else {
85                 fail("Could not match line from readme: " + entry.getValue());
86             }
87         }
88         assertThat("Expected number created channels from readme doesn't match with source code", list.size(),
89                 is(EXPECTED_NUMBER_OF_CHANNELS));
90         final List<String> channelsFromXml = readThingsXml(CHANNEL_PATTERN, THING_TYPES_XML);
91         final List<String> channelsFromReadme = list.stream().map(String::trim).sorted().collect(Collectors.toList());
92         for (int i = 0; i < channelsFromXml.size(); i++) {
93             assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
94         }
95     }
96
97     /**
98      * Test is the channel-type matches with the description in the README.
99      * This test is a little verbose as it generates the channel-type description as in the xml is specified.
100      * This is for easy adding more channels, by simply adding them to the readme and copy-paste the generated xml to
101      * the channels xml.
102      */
103     @Test
104     public void testNutNameMatchingReadme() {
105         final Map<NutName, String> readMeNutNames = readReadme();
106         final List<String> list = new ArrayList<>();
107
108         for (final NutName nn : NutName.values()) {
109             buildChannel(list, nn, readMeNutNames.get(nn));
110         }
111         assertThat("Expected number created channel data from readme doesn't match with source code", list.size(),
112                 is(EXPECTED_NUMMBER_OF_CHANNEL_XML_LINES));
113         final List<String> channelsFromXml = readThingsXml(CHANNEL_TYPE_PATTERN, CHANNELS_XML);
114         final List<String> channelsFromReadme = list.stream().map(String::trim).sorted().collect(Collectors.toList());
115
116         for (int i = 0; i < channelsFromXml.size(); i++) {
117             assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
118         }
119     }
120
121     private Map<NutName, String> readReadme() {
122         try {
123             final String path = Path.of(getClass().getProtectionDomain().getClassLoader().getResource(".").toURI())
124                     .toString();
125             final List<String> lines = Files.readAllLines(Path.of(path, "..", "..", "README.md"));
126
127             return lines.stream().filter(line -> README_PATTERN.matcher(line).find())
128                     .collect(Collectors.toMap(this::lineToNutName, Function.identity()));
129         } catch (final IOException | URISyntaxException e) {
130             fail("Could not read README.md");
131             return null;
132         }
133     }
134
135     private List<String> readThingsXml(final Pattern pattern, final String filename) {
136         try {
137             final String path = Path.of(getClass().getProtectionDomain().getClassLoader().getResource(".").toURI())
138                     .toString();
139             final List<String> lines = Files
140                     .readAllLines(Path.of(path, "..", "..", "src", "main", "resources", "OH-INF", "thing", filename));
141             return lines.stream().filter(line -> pattern.matcher(line).find()).map(String::trim).sorted()
142                     .collect(Collectors.toList());
143         } catch (final IOException | URISyntaxException e) {
144             fail("Could not read things xml");
145             return null;
146         }
147     }
148
149     private NutName lineToNutName(final String line) {
150         final Matcher matcher = README_PATTERN.matcher(line);
151         assertTrue(matcher.find(), "Could not match readme line: " + line);
152         final String name = matcher.group(1);
153         final NutName channelIdToNutName = NutName.channelIdToNutName(name);
154         assertNotNull(channelIdToNutName, "Name should not match null: '" + name + "' ->" + line);
155         return channelIdToNutName;
156     }
157
158     private void buildChannel(final List<String> list, final NutName nn, final String readmeLine) {
159         if (readmeLine == null) {
160             fail("Readme line is null for: " + nn);
161         } else {
162             final Matcher matcher = README_PATTERN.matcher(readmeLine);
163
164             if (matcher.find()) {
165                 final String advanced = README_IS_ADVANCED.equals(matcher.group(5)) ? TEMPLATE_ADVANCED : "";
166
167                 list.add(String.format(TEMPLATE_CHANNEL_TYPE, nutNameToChannelType(nn), advanced));
168                 final String itemType = matcher.group(2);
169
170                 list.add(String.format(TEMPLATE_ITEM_TYPE, itemType));
171                 list.add(String.format(TEMPLATE_LABEL, nutNameToLabel(nn)));
172                 list.add(String.format(TEMPLATE_DESCRIPTION, matcher.group(4).trim()));
173                 final String pattern = nutNameToPattern(itemType);
174
175                 list.add(pattern.isEmpty()
176                         ? NutName.UPS_STATUS == nn ? TEMPLATE_STATE_OPTIONS : TEMPLATE_STATE_NO_PATTERN
177                         : String.format(TEMPLATE_STATE, pattern));
178             } else {
179                 fail("Could not parse the line from README:" + readmeLine);
180             }
181             list.add(TEMPLATE_CHANNEL_TYPE_END);
182         }
183     }
184
185     private String nutNameToLabel(final NutName nn) {
186         final String[] labelWords = nn.getName().replace("ups", "UPS").split("\\.");
187         return Stream.of(labelWords).map(w -> Character.toUpperCase(w.charAt(0)) + w.substring(1))
188                 .collect(Collectors.joining(" "));
189     }
190
191     private String nutNameToChannelType(final NutName nn) {
192         return nn.getName().replace('.', '-');
193     }
194
195     private String nutNameToPattern(final String itemType) {
196         final String pattern;
197         switch (itemType) {
198             case CoreItemFactory.STRING:
199                 pattern = "";
200                 break;
201             case CoreItemFactory.NUMBER:
202                 pattern = "%d";
203                 break;
204             case "Number:Dimensionless":
205                 pattern = "%.1f %%";
206                 break;
207             case "Number:Time":
208                 pattern = "%d %unit%";
209                 break;
210             case "Number:Power":
211             case "Number:ElectricPotential":
212                 pattern = "%.0f %unit%";
213                 break;
214             case "Number:Temperature":
215             case "Number:ElectricCurrent":
216             case "Number:Frequency":
217             case "Number:Angle":
218                 pattern = "%.1f %unit%";
219                 break;
220             default:
221                 fail("itemType not supported:" + itemType);
222                 pattern = "";
223                 break;
224         }
225         return pattern;
226     }
227 }