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