emqx_topic_SUITE.erl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2017-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_topic_SUITE).
  17. -compile(export_all).
  18. -compile(nowarn_export_all).
  19. -include_lib("eunit/include/eunit.hrl").
  20. -include_lib("emqx/include/emqx_mqtt.hrl").
  21. -include_lib("emqx/include/emqx_placeholder.hrl").
  22. -import(
  23. emqx_topic,
  24. [
  25. wildcard/1,
  26. match/2,
  27. validate/1,
  28. prepend/2,
  29. join/1,
  30. words/1,
  31. systop/1,
  32. feed_var/3,
  33. parse/1,
  34. parse/2
  35. ]
  36. ).
  37. -define(N, 100000).
  38. all() -> emqx_common_test_helpers:all(?MODULE).
  39. t_wildcard(_) ->
  40. true = wildcard(<<"a/b/#">>),
  41. true = wildcard(<<"a/+/#">>),
  42. false = wildcard(<<"">>),
  43. false = wildcard(<<"a/b/c">>).
  44. t_match1(_) ->
  45. true = match(<<"a/b/c">>, <<"a/b/+">>),
  46. true = match(<<"a/b/c">>, <<"a/#">>),
  47. true = match(<<"abcd/ef/g">>, <<"#">>),
  48. true = match(<<"abc/de/f">>, <<"abc/de/f">>),
  49. true = match(<<"abc">>, <<"+">>),
  50. true = match(<<"a/b/c">>, <<"a/b/c">>),
  51. false = match(<<"a/b/c">>, <<"a/c/d">>),
  52. false = match(<<"$share/x/y">>, <<"+">>),
  53. false = match(<<"$share/x/y">>, <<"+/x/y">>),
  54. false = match(<<"$share/x/y">>, <<"#">>),
  55. false = match(<<"$share/x/y">>, <<"+/+/#">>),
  56. false = match(<<"house/1/sensor/0">>, <<"house/+">>),
  57. false = match(<<"house">>, <<"house/+">>).
  58. t_match2(_) ->
  59. true = match(<<"sport/tennis/player1">>, <<"sport/tennis/player1/#">>),
  60. true = match(<<"sport/tennis/player1/ranking">>, <<"sport/tennis/player1/#">>),
  61. true = match(<<"sport/tennis/player1/score/wimbledon">>, <<"sport/tennis/player1/#">>),
  62. true = match(<<"sport">>, <<"sport/#">>),
  63. true = match(<<"sport">>, <<"#">>),
  64. true = match(<<"/sport/football/score/1">>, <<"#">>),
  65. true = match(<<"Topic/C">>, <<"+/+">>),
  66. true = match(<<"TopicA/B">>, <<"+/+">>),
  67. true = match(<<"TopicA/C">>, <<"+/+">>),
  68. true = match(<<"abc">>, <<"+">>),
  69. true = match(<<"a/b/c">>, <<"a/b/c">>),
  70. false = match(<<"a/b/c">>, <<"a/c/d">>),
  71. false = match(<<"$share/x/y">>, <<"+">>),
  72. false = match(<<"$share/x/y">>, <<"+/x/y">>),
  73. false = match(<<"$share/x/y">>, <<"#">>),
  74. false = match(<<"$share/x/y">>, <<"+/+/#">>),
  75. false = match(<<"house/1/sensor/0">>, <<"house/+">>).
  76. t_match3(_) ->
  77. true = match(<<"device/60019423a83c/fw">>, <<"device/60019423a83c/#">>),
  78. true = match(<<"device/60019423a83c/$fw">>, <<"device/60019423a83c/#">>),
  79. true = match(<<"device/60019423a83c/$fw/fw">>, <<"device/60019423a83c/$fw/#">>),
  80. true = match(<<"device/60019423a83c/fw/checksum">>, <<"device/60019423a83c/#">>),
  81. true = match(<<"device/60019423a83c/$fw/checksum">>, <<"device/60019423a83c/#">>),
  82. true = match(<<"device/60019423a83c/dust/type">>, <<"device/60019423a83c/#">>).
  83. t_sigle_level_match(_) ->
  84. true = match(<<"sport/tennis/player1">>, <<"sport/tennis/+">>),
  85. false = match(<<"sport/tennis/player1/ranking">>, <<"sport/tennis/+">>),
  86. false = match(<<"sport">>, <<"sport/+">>),
  87. true = match(<<"sport/">>, <<"sport/+">>),
  88. true = match(<<"/finance">>, <<"+/+">>),
  89. true = match(<<"/finance">>, <<"/+">>),
  90. false = match(<<"/finance">>, <<"+">>),
  91. true = match(<<"/devices/$dev1">>, <<"/devices/+">>),
  92. true = match(<<"/devices/$dev1/online">>, <<"/devices/+/online">>).
  93. t_sys_match(_) ->
  94. true = match(<<"$SYS/broker/clients/testclient">>, <<"$SYS/#">>),
  95. true = match(<<"$SYS/broker">>, <<"$SYS/+">>),
  96. false = match(<<"$SYS/broker">>, <<"+/+">>),
  97. false = match(<<"$SYS/broker">>, <<"#">>).
  98. 't_#_match'(_) ->
  99. true = match(<<"a/b/c">>, <<"#">>),
  100. true = match(<<"a/b/c">>, <<"+/#">>),
  101. false = match(<<"$SYS/brokers">>, <<"#">>),
  102. true = match(<<"a/b/$c">>, <<"a/b/#">>),
  103. true = match(<<"a/b/$c">>, <<"a/#">>).
  104. t_match_tokens(_) ->
  105. true = match(emqx_topic:tokens(<<"a/b/c">>), words(<<"a/+/c">>)),
  106. true = match(emqx_topic:tokens(<<"a//c">>), words(<<"a/+/c">>)),
  107. false = match(emqx_topic:tokens(<<"a//c/">>), words(<<"a/+/c">>)),
  108. true = match(emqx_topic:tokens(<<"a//c/">>), words(<<"a/+/c/#">>)).
  109. t_match_perf(_) ->
  110. true = match(<<"a/b/ccc">>, <<"a/#">>),
  111. Name = <<"/abkc/19383/192939/akakdkkdkak/xxxyyuya/akakak">>,
  112. Filter = <<"/abkc/19383/+/akakdkkdkak/#">>,
  113. true = match(Name, Filter),
  114. ok = bench('match/2', fun emqx_topic:match/2, [Name, Filter]).
  115. t_validate(_) ->
  116. true = validate(<<"a/+/#">>),
  117. true = validate(<<"a/b/c/d">>),
  118. true = validate({name, <<"abc/de/f">>}),
  119. true = validate({filter, <<"abc/+/f">>}),
  120. true = validate({filter, <<"abc/#">>}),
  121. true = validate({filter, <<"x">>}),
  122. true = validate({name, <<"x//y">>}),
  123. true = validate({filter, <<"sport/tennis/#">>}),
  124. %% MQTT-5.0 [MQTT-4.7.3-1]
  125. ?assertError(empty_topic, validate({name, <<>>})),
  126. ?assertError(empty_topic, validate({filter, <<>>})),
  127. ?assertError(topic_name_error, validate({name, <<"abc/#">>})),
  128. ?assertError(topic_too_long, validate({name, long_topic()})),
  129. ?assertError(topic_invalid_char, validate({filter, <<"abc/#xzy/+">>})),
  130. ?assertError(topic_invalid_char, validate({filter, <<"abc/xzy/+9827">>})),
  131. ?assertError(topic_invalid_char, validate({filter, <<"sport/tennis#">>})),
  132. %% MQTT-5.0 [MQTT-4.7.1-1]
  133. ?assertError('topic_invalid_#', validate({filter, <<"abc/#/1">>})),
  134. ?assertError('topic_invalid_#', validate({filter, <<"sport/tennis/#/ranking">>})),
  135. %% MQTT-5.0 [MQTT-4.8.2-1]
  136. ?assertError(?SHARE_EMPTY_FILTER, validate({filter, <<"$share/">>})),
  137. ?assertError(?SHARE_EMPTY_FILTER, validate({filter, <<"$share//">>})),
  138. ?assertError(?SHARE_EMPTY_GROUP, validate({filter, <<"$share//t">>})),
  139. ?assertError(?SHARE_EMPTY_GROUP, validate({filter, <<"$share//test">>})),
  140. %% MQTT-5.0 [MQTT-4.7.3-1] for shared-sub
  141. ?assertError(?SHARE_EMPTY_FILTER, validate({filter, <<"$share/g/">>})),
  142. ?assertError(?SHARE_EMPTY_FILTER, validate({filter, <<"$share/g2/">>})),
  143. %% MQTT-5.0 [MQTT-4.8.2-2]
  144. ?assertError(?SHARE_NAME_INVALID_CHAR, validate({filter, <<"$share/p+q/1">>})),
  145. ?assertError(?SHARE_NAME_INVALID_CHAR, validate({filter, <<"$share/m+/1">>})),
  146. ?assertError(?SHARE_NAME_INVALID_CHAR, validate({filter, <<"$share/+n/1">>})),
  147. ?assertError(?SHARE_NAME_INVALID_CHAR, validate({filter, <<"$share/x#y/1">>})),
  148. ?assertError(?SHARE_NAME_INVALID_CHAR, validate({filter, <<"$share/x#/1">>})),
  149. ?assertError(?SHARE_NAME_INVALID_CHAR, validate({filter, <<"$share/#y/1">>})),
  150. %% share recursively
  151. ?assertError(?SHARE_RECURSIVELY, validate({filter, <<"$share/g1/$share/t">>})),
  152. true = validate({filter, <<"$share/g1/topic/$share">>}).
  153. t_sigle_level_validate(_) ->
  154. true = validate({filter, <<"+">>}),
  155. true = validate({filter, <<"+/tennis/#">>}),
  156. true = validate({filter, <<"sport/+/player1">>}),
  157. ?assertError(topic_invalid_char, validate({filter, <<"sport+">>})).
  158. t_prepend(_) ->
  159. ?assertEqual(<<"ab">>, prepend(undefined, <<"ab">>)),
  160. ?assertEqual(<<"a/b">>, prepend(<<>>, <<"a/b">>)),
  161. ?assertEqual(<<"x/a/b">>, prepend("x/", <<"a/b">>)),
  162. ?assertEqual(<<"x/y/a/b">>, prepend(<<"x/y">>, <<"a/b">>)),
  163. ?assertEqual(<<"+/a/b">>, prepend('+', <<"a/b">>)).
  164. t_levels(_) ->
  165. ?assertEqual(3, emqx_topic:levels(<<"a/+/#">>)),
  166. ?assertEqual(4, emqx_topic:levels(<<"a/b/c/d">>)).
  167. t_tokens(_) ->
  168. ?assertEqual(
  169. [<<"a">>, <<"b">>, <<"+">>, <<"#">>],
  170. emqx_topic:tokens(<<"a/b/+/#">>)
  171. ).
  172. t_words(_) ->
  173. Topic = <<"/abkc/19383/+/akakdkkdkak/#">>,
  174. ?assertEqual(['', <<"a">>, '+', '#'], words(<<"/a/+/#">>)),
  175. ?assertEqual(['', <<"abkc">>, <<"19383">>, '+', <<"akakdkkdkak">>, '#'], words(Topic)),
  176. ok = bench('words/1', fun emqx_topic:words/1, [Topic]),
  177. BSplit = fun(Bin) -> binary:split(Bin, <<"/">>, [global]) end,
  178. ok = bench('binary:split/3', BSplit, [Topic]).
  179. t_join(_) ->
  180. ?assertEqual(<<>>, join([])),
  181. ?assertEqual(<<"x">>, join([<<"x">>])),
  182. ?assertEqual(<<"#">>, join(['#'])),
  183. ?assertEqual(<<"+//#">>, join(['+', '', '#'])),
  184. ?assertEqual(<<"x/y/z/+">>, join([<<"x">>, <<"y">>, <<"z">>, '+'])),
  185. ?assertEqual(<<"/ab/cd/ef/">>, join(words(<<"/ab/cd/ef/">>))),
  186. ?assertEqual(<<"ab/+/#">>, join(words(<<"ab/+/#">>))),
  187. %% MQTT-5.0 [MQTT-4.7.1-1]
  188. ?assertError('topic_invalid_#', join(['+', <<"a">>, '#', <<"b">>, '', '+'])),
  189. ?assertError('topic_invalid_#', join(['+', <<"c">>, <<"#">>, <<"d">>, '', '+'])).
  190. t_systop(_) ->
  191. SysTop1 = iolist_to_binary(["$SYS/brokers/", atom_to_list(node()), "/xyz"]),
  192. ?assertEqual(SysTop1, systop('xyz')),
  193. SysTop2 = iolist_to_binary(["$SYS/brokers/", atom_to_list(node()), "/abc"]),
  194. ?assertEqual(SysTop2, systop(<<"abc">>)).
  195. t_feed_var(_) ->
  196. ?assertEqual(
  197. <<"$queue/client/clientId">>,
  198. feed_var(<<"$c">>, <<"clientId">>, <<"$queue/client/$c">>)
  199. ),
  200. ?assertEqual(
  201. <<"username/test/client/x">>,
  202. feed_var(
  203. ?PH_USERNAME,
  204. <<"test">>,
  205. <<"username/", ?PH_USERNAME/binary, "/client/x">>
  206. )
  207. ),
  208. ?assertEqual(
  209. <<"username/test/client/clientId">>,
  210. feed_var(
  211. ?PH_CLIENTID,
  212. <<"clientId">>,
  213. <<"username/test/client/", ?PH_CLIENTID/binary>>
  214. )
  215. ).
  216. long_topic() ->
  217. iolist_to_binary([[integer_to_list(I), "/"] || I <- lists:seq(0, 66666)]).
  218. t_parse(_) ->
  219. ?assertError(
  220. {invalid_topic_filter, <<"$queue/t">>},
  221. parse(#share{group = <<"$queue">>, topic = <<"$queue/t">>}, #{})
  222. ),
  223. ?assertError(
  224. {invalid_topic_filter, <<"$share/g/t">>},
  225. parse(#share{group = <<"g">>, topic = <<"$share/g/t">>}, #{})
  226. ),
  227. ?assertError(
  228. {invalid_topic_filter, <<"$share/t">>},
  229. parse(<<"$share/t">>)
  230. ),
  231. ?assertError(
  232. {invalid_topic_filter, <<"$share/+/t">>},
  233. parse(<<"$share/+/t">>)
  234. ),
  235. ?assertEqual({<<"a/b/+/#">>, #{}}, parse(<<"a/b/+/#">>)),
  236. ?assertEqual({<<"a/b/+/#">>, #{qos => 1}}, parse({<<"a/b/+/#">>, #{qos => 1}})),
  237. ?assertEqual(
  238. {#share{group = <<"$queue">>, topic = <<"topic">>}, #{}}, parse(<<"$queue/topic">>)
  239. ),
  240. ?assertEqual(
  241. {#share{group = <<"group">>, topic = <<"topic">>}, #{}}, parse(<<"$share/group/topic">>)
  242. ),
  243. %% The '$local' and '$fastlane' topics have been deprecated.
  244. ?assertEqual({<<"$local/topic">>, #{}}, parse(<<"$local/topic">>)),
  245. ?assertEqual({<<"$local/$queue/topic">>, #{}}, parse(<<"$local/$queue/topic">>)),
  246. ?assertEqual({<<"$local/$share/group/a/b/c">>, #{}}, parse(<<"$local/$share/group/a/b/c">>)),
  247. ?assertEqual({<<"$fastlane/topic">>, #{}}, parse(<<"$fastlane/topic">>)).
  248. bench(Case, Fun, Args) ->
  249. {Time, ok} = timer:tc(
  250. fun lists:foreach/2,
  251. [
  252. fun(_) -> apply(Fun, Args) end,
  253. lists:seq(1, ?N)
  254. ]
  255. ),
  256. ct:pal(
  257. "Time consumed by ~ts: ~.3f(us)~nCall ~ts per second: ~w",
  258. [Case, Time / ?N, Case, (?N * 1000000) div Time]
  259. ).