2 * Copyright (c) 2010-2020 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.*;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.List;
24 import java.util.Map.Entry;
25 import java.util.function.Function;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
31 import org.apache.commons.io.FileUtils;
32 import org.junit.jupiter.api.Test;
33 import org.openhab.core.library.CoreItemFactory;
36 * Test class that reads the README.md and matches it with the OH-INF thing channel definitions.
38 * @author Hilbrand Bouwkamp - Initial contribution
40 public class NutNameChannelsTest {
42 private static final String THING_TYPES_XML = "thing-types.xml";
43 private static final String CHANNELS_XML = "channels.xml";
45 private static final int EXPECTED_NUMBER_OF_CHANNELS = 20;
46 private static final int EXPECTED_NUMMBER_OF_CHANNEL_XML_LINES = EXPECTED_NUMBER_OF_CHANNELS * 6;
48 // README table is: | Channel Name | Item Type | Unit | Description | Advanced
49 private static final Pattern README_PATTERN = Pattern
50 .compile("^\\|\\s+([\\w\\.]+)\\s+\\|\\s+([:\\w]+)\\s+\\|\\s+([^\\|]+)\\|\\s+([^\\|]+)\\|\\s+([^\\s]+)");
51 private static final Pattern CHANNEL_PATTERN = Pattern.compile("<channel id");
52 private static final Pattern CHANNEL_TYPE_PATTERN = Pattern
53 .compile("(<channel-type|<item-type|<label|<description|<state|</channel-type)");
55 private static final String TEMPLATE_CHANNEL_TYPE = "<channel-type id=\"%s\"%s>";
56 private static final String TEMPLATE_ADVANCED = " advanced=\"true\"";
57 private static final String TEMPLATE_ITEM_TYPE = "<item-type>%s</item-type>";
58 private static final String TEMPLATE_LABEL = "<label>%s</label>";
59 private static final String TEMPLATE_DESCRIPTION = "<description>%s</description>";
60 private static final String TEMPLATE_STATE = "<state pattern=\"%s\" readOnly=\"true\"/>";
61 private static final String TEMPLATE_STATE_NO_PATTERN = "<state readOnly=\"true\"/>";
62 private static final String TEMPLATE_STATE_OPTIONS = "<state readOnly=\"true\">";
63 private static final String TEMPLATE_CHANNEL_TYPE_END = "</channel-type>";
64 private static final String TEMPLATE_CHANNEL = "<channel id=\"%s\" typeId=\"%s\"/>";
66 private static final String README_IS_ADVANCED = "yes";
69 * Test if README matches with the channels in the things xml.
72 public void testReadmeMatchingChannels() {
73 final Map<NutName, String> readMeNutNames = readReadme();
74 final List<String> list = new ArrayList<>();
76 for (final Entry<NutName, String> entry : readMeNutNames.entrySet()) {
77 final Matcher matcher = README_PATTERN.matcher(entry.getValue());
79 assertNotNull(entry.getKey(), "Could not find NutName in readme for : " + entry.getValue());
81 list.add(String.format(TEMPLATE_CHANNEL, entry.getKey().getChannelId(),
82 nutNameToChannelType(entry.getKey())));
84 fail("Could not match line from readme: " + entry.getValue());
87 assertThat("Expected number created channels from readme doesn't match with source code", list.size(),
88 is(EXPECTED_NUMBER_OF_CHANNELS));
89 final List<String> channelsFromXml = readThingsXml(CHANNEL_PATTERN, THING_TYPES_XML);
90 final List<String> channelsFromReadme = list.stream().map(String::trim).sorted().collect(Collectors.toList());
91 for (int i = 0; i < channelsFromXml.size(); i++) {
92 assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
97 * Test is the channel-type matches with the description in the README.
98 * This test is a little verbose as it generates the channel-type description as in the xml is specified.
99 * This is for easy adding more channels, by simply adding them to the readme and copy-paste the generated xml to
103 public void testNutNameMatchingReadme() {
104 final Map<NutName, String> readMeNutNames = readReadme();
105 final List<String> list = new ArrayList<>();
107 for (final NutName nn : NutName.values()) {
108 buildChannel(list, nn, readMeNutNames.get(nn));
110 assertThat("Expected number created channel data from readme doesn't match with source code", list.size(),
111 is(EXPECTED_NUMMBER_OF_CHANNEL_XML_LINES));
112 final List<String> channelsFromXml = readThingsXml(CHANNEL_TYPE_PATTERN, CHANNELS_XML);
113 final List<String> channelsFromReadme = list.stream().map(String::trim).sorted().collect(Collectors.toList());
115 for (int i = 0; i < channelsFromXml.size(); i++) {
116 assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
120 private Map<NutName, String> readReadme() {
121 final String path = getClass().getProtectionDomain().getClassLoader().getResource(".").getFile() + "../..";
124 final List<String> lines = FileUtils.readLines(new File(path, "README.md"));
126 return lines.stream().filter(line -> README_PATTERN.matcher(line).find())
127 .collect(Collectors.toMap(this::lineToNutName, Function.identity()));
128 } catch (final IOException e) {
129 fail("Could not read README.md from: " + path);
134 private List<String> readThingsXml(final Pattern pattern, final String filename) {
135 final String path = getClass().getProtectionDomain().getClassLoader().getResource(".").getFile()
136 + "../../src/main/resources/OH-INF/thing";
138 final List<String> lines = FileUtils.readLines(new File(path, filename));
139 return lines.stream().filter(line -> pattern.matcher(line).find()).map(String::trim).sorted()
140 .collect(Collectors.toList());
141 } catch (final IOException e) {
142 fail("Could not read things xml from: " + path);
147 private NutName lineToNutName(final String line) {
148 final Matcher matcher = README_PATTERN.matcher(line);
149 assertTrue(matcher.find(), "Could not match readme line: " + line);
150 final String name = matcher.group(1);
151 final NutName channelIdToNutName = NutName.channelIdToNutName(name);
152 assertNotNull(channelIdToNutName, "Name should not match null: '" + name + "' ->" + line);
153 return channelIdToNutName;
156 private void buildChannel(final List<String> list, final NutName nn, final String readmeLine) {
157 if (readmeLine == null) {
158 fail("Readme line is null for: " + nn);
160 final Matcher matcher = README_PATTERN.matcher(readmeLine);
162 if (matcher.find()) {
163 final String advanced = README_IS_ADVANCED.equals(matcher.group(5)) ? TEMPLATE_ADVANCED : "";
165 list.add(String.format(TEMPLATE_CHANNEL_TYPE, nutNameToChannelType(nn), advanced));
166 final String itemType = matcher.group(2);
168 list.add(String.format(TEMPLATE_ITEM_TYPE, itemType));
169 list.add(String.format(TEMPLATE_LABEL, nutNameToLabel(nn)));
170 list.add(String.format(TEMPLATE_DESCRIPTION, matcher.group(4).trim()));
171 final String pattern = nutNameToPattern(itemType);
173 list.add(pattern.isEmpty()
174 ? NutName.UPS_STATUS == nn ? TEMPLATE_STATE_OPTIONS : TEMPLATE_STATE_NO_PATTERN
175 : String.format(TEMPLATE_STATE, pattern));
177 fail("Could not parse the line from README:" + readmeLine);
179 list.add(TEMPLATE_CHANNEL_TYPE_END);
183 private String nutNameToLabel(final NutName nn) {
184 final String[] labelWords = nn.getName().replace("ups", "UPS").split("\\.");
185 return Stream.of(labelWords).map(w -> Character.toUpperCase(w.charAt(0)) + w.substring(1))
186 .collect(Collectors.joining(" "));
189 private String nutNameToChannelType(final NutName nn) {
190 return nn.getName().replace('.', '-');
193 private String nutNameToPattern(final String itemType) {
194 final String pattern;
196 case CoreItemFactory.STRING:
199 case CoreItemFactory.NUMBER:
202 case "Number:Dimensionless":
206 pattern = "%d %unit%";
209 case "Number:ElectricPotential":
210 pattern = "%.0f %unit%";
212 case "Number:Temperature":
213 case "Number:ElectricCurrent":
214 case "Number:Frequency":
216 pattern = "%.1f %unit%";
219 fail("itemType not supported:" + itemType);