]> git.basschouten.com Git - openhab-addons.git/blob
00ed5d06460c782e6bedf7d9ab0a9e519f2206ce
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.File;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map;
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;
30
31 import org.apache.commons.io.FileUtils;
32 import org.junit.jupiter.api.Test;
33 import org.openhab.core.library.CoreItemFactory;
34
35 /**
36  * Test class that reads the README.md and matches it with the OH-INF thing channel definitions.
37  *
38  * @author Hilbrand Bouwkamp - Initial contribution
39  */
40 public class NutNameChannelsTest {
41
42     private static final String THING_TYPES_XML = "thing-types.xml";
43     private static final String CHANNELS_XML = "channels.xml";
44
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;
47
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)");
54
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\"/>";
65
66     private static final String README_IS_ADVANCED = "yes";
67
68     /**
69      * Test if README matches with the channels in the things xml.
70      */
71     @Test
72     public void testReadmeMatchingChannels() {
73         final Map<NutName, String> readMeNutNames = readReadme();
74         final List<String> list = new ArrayList<>();
75
76         for (final Entry<NutName, String> entry : readMeNutNames.entrySet()) {
77             final Matcher matcher = README_PATTERN.matcher(entry.getValue());
78
79             assertNotNull(entry.getKey(), "Could not find NutName in readme for : " + entry.getValue());
80             if (matcher.find()) {
81                 list.add(String.format(TEMPLATE_CHANNEL, entry.getKey().getChannelId(),
82                         nutNameToChannelType(entry.getKey())));
83             } else {
84                 fail("Could not match line from readme: " + entry.getValue());
85             }
86         }
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)));
93         }
94     }
95
96     /**
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
100      * the channels xml.
101      */
102     @Test
103     public void testNutNameMatchingReadme() {
104         final Map<NutName, String> readMeNutNames = readReadme();
105         final List<String> list = new ArrayList<>();
106
107         for (final NutName nn : NutName.values()) {
108             buildChannel(list, nn, readMeNutNames.get(nn));
109         }
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());
114
115         for (int i = 0; i < channelsFromXml.size(); i++) {
116             assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
117         }
118     }
119
120     private Map<NutName, String> readReadme() {
121         final String path = getClass().getProtectionDomain().getClassLoader().getResource(".").getFile() + "../..";
122
123         try {
124             final List<String> lines = FileUtils.readLines(new File(path, "README.md"));
125
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);
130             return null;
131         }
132     }
133
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";
137         try {
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);
143             return null;
144         }
145     }
146
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;
154     }
155
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);
159         } else {
160             final Matcher matcher = README_PATTERN.matcher(readmeLine);
161
162             if (matcher.find()) {
163                 final String advanced = README_IS_ADVANCED.equals(matcher.group(5)) ? TEMPLATE_ADVANCED : "";
164
165                 list.add(String.format(TEMPLATE_CHANNEL_TYPE, nutNameToChannelType(nn), advanced));
166                 final String itemType = matcher.group(2);
167
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);
172
173                 list.add(pattern.isEmpty()
174                         ? NutName.UPS_STATUS == nn ? TEMPLATE_STATE_OPTIONS : TEMPLATE_STATE_NO_PATTERN
175                         : String.format(TEMPLATE_STATE, pattern));
176             } else {
177                 fail("Could not parse the line from README:" + readmeLine);
178             }
179             list.add(TEMPLATE_CHANNEL_TYPE_END);
180         }
181     }
182
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(" "));
187     }
188
189     private String nutNameToChannelType(final NutName nn) {
190         return nn.getName().replace('.', '-');
191     }
192
193     private String nutNameToPattern(final String itemType) {
194         final String pattern;
195         switch (itemType) {
196             case CoreItemFactory.STRING:
197                 pattern = "";
198                 break;
199             case CoreItemFactory.NUMBER:
200                 pattern = "%d";
201                 break;
202             case "Number:Dimensionless":
203                 pattern = "%.1f %%";
204                 break;
205             case "Number:Time":
206                 pattern = "%d %unit%";
207                 break;
208             case "Number:Power":
209             case "Number:ElectricPotential":
210                 pattern = "%.0f %unit%";
211                 break;
212             case "Number:Temperature":
213             case "Number:ElectricCurrent":
214             case "Number:Frequency":
215             case "Number:Angle":
216                 pattern = "%.1f %unit%";
217                 break;
218             default:
219                 fail("itemType not supported:" + itemType);
220                 pattern = "";
221                 break;
222         }
223         return pattern;
224     }
225 }