2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.networkupstools.internal;
15 import static org.hamcrest.MatcherAssert.assertThat;
16 import static org.hamcrest.Matchers.is;
17 import static org.junit.jupiter.api.Assertions.*;
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;
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;
33 import org.junit.jupiter.api.Test;
34 import org.openhab.core.library.CoreItemFactory;
37 * Test class that reads the README.md and matches it with the OH-INF thing channel definitions.
39 * @author Hilbrand Bouwkamp - Initial contribution
41 public class NutNameChannelsTest {
43 private static final String THING_TYPES_XML = "thing-types.xml";
44 private static final String CHANNELS_XML = "channels.xml";
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;
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)");
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\"/>";
67 private static final String README_IS_ADVANCED = "yes";
70 * Test if README matches with the channels in the things xml.
73 public void testReadmeMatchingChannels() {
74 final Map<NutName, String> readMeNutNames = readReadme();
75 final List<String> list = new ArrayList<>();
77 for (final Entry<NutName, String> entry : readMeNutNames.entrySet()) {
78 final Matcher matcher = README_PATTERN.matcher(entry.getValue());
80 assertNotNull(entry.getKey(), "Could not find NutName in readme for : " + entry.getValue());
82 list.add(String.format(TEMPLATE_CHANNEL, entry.getKey().getChannelId(),
83 nutNameToChannelType(entry.getKey())));
85 fail("Could not match line from readme: " + entry.getValue());
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)));
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
104 public void testNutNameMatchingReadme() {
105 final Map<NutName, String> readMeNutNames = readReadme();
106 final List<String> list = new ArrayList<>();
108 for (final NutName nn : NutName.values()) {
109 buildChannel(list, nn, readMeNutNames.get(nn));
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());
116 for (int i = 0; i < channelsFromXml.size(); i++) {
117 assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
121 private Map<NutName, String> readReadme() {
123 final String path = Path.of(getClass().getProtectionDomain().getClassLoader().getResource(".").toURI())
125 final List<String> lines = Files.readAllLines(Path.of(path, "..", "..", "README.md"));
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");
135 private List<String> readThingsXml(final Pattern pattern, final String filename) {
137 final String path = Path.of(getClass().getProtectionDomain().getClassLoader().getResource(".").toURI())
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");
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;
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);
162 final Matcher matcher = README_PATTERN.matcher(readmeLine);
164 if (matcher.find()) {
165 final String advanced = README_IS_ADVANCED.equals(matcher.group(5)) ? TEMPLATE_ADVANCED : "";
167 list.add(String.format(TEMPLATE_CHANNEL_TYPE, nutNameToChannelType(nn), advanced));
168 final String itemType = matcher.group(2);
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);
175 list.add(pattern.isEmpty()
176 ? NutName.UPS_STATUS == nn ? TEMPLATE_STATE_OPTIONS : TEMPLATE_STATE_NO_PATTERN
177 : String.format(TEMPLATE_STATE, pattern));
179 fail("Could not parse the line from README:" + readmeLine);
181 list.add(TEMPLATE_CHANNEL_TYPE_END);
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(" "));
191 private String nutNameToChannelType(final NutName nn) {
192 return nn.getName().replace('.', '-');
195 private String nutNameToPattern(final String itemType) {
196 final String pattern;
198 case CoreItemFactory.STRING:
201 case CoreItemFactory.NUMBER:
204 case "Number:Dimensionless":
208 pattern = "%d %unit%";
211 case "Number:ElectricPotential":
212 pattern = "%.0f %unit%";
214 case "Number:Temperature":
215 case "Number:ElectricCurrent":
216 case "Number:Frequency":
218 pattern = "%.1f %unit%";
221 fail("itemType not supported:" + itemType);