graph_parser_test.cljs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. (ns logseq.graph-parser-test
  2. (:require [cljs.test :refer [deftest testing is are]]
  3. [clojure.string :as string]
  4. [logseq.graph-parser :as graph-parser]
  5. [logseq.graph-parser.db :as gp-db]
  6. [logseq.graph-parser.block :as gp-block]
  7. [logseq.graph-parser.property :as gp-property]
  8. [datascript.core :as d]
  9. [logseq.db :as ldb]))
  10. (def foo-edn
  11. "Example exported whiteboard page as an edn exportable."
  12. '{:blocks
  13. ({:block/content "foo content a",
  14. :block/format :markdown
  15. :block/parent {:block/uuid #uuid "16c90195-6a03-4b3f-839d-095a496d9acd"}},
  16. {:block/content "foo content b",
  17. :block/format :markdown
  18. :block/parent {:block/uuid #uuid "16c90195-6a03-4b3f-839d-095a496d9acd"}}),
  19. :pages
  20. ({:block/format :markdown,
  21. :block/name "foo"
  22. :block/original-name "Foo"
  23. :block/uuid #uuid "16c90195-6a03-4b3f-839d-095a496d9acd"
  24. :block/properties {:title "my whiteboard foo"}})})
  25. (def foo-conflict-edn
  26. "Example exported whiteboard page as an edn exportable."
  27. '{:blocks
  28. ({:block/content "foo content a",
  29. :block/format :markdown},
  30. {:block/content "foo content b",
  31. :block/format :markdown}),
  32. :pages
  33. ({:block/format :markdown,
  34. :block/name "foo conflicted"
  35. :block/original-name "Foo conflicted"
  36. :block/uuid #uuid "16c90195-6a03-4b3f-839d-095a496d9acd"})})
  37. (def bar-edn
  38. "Example exported whiteboard page as an edn exportable."
  39. '{:blocks
  40. ({:block/content "foo content a",
  41. :block/format :markdown
  42. :block/parent {:block/uuid #uuid "71515b7d-b5fc-496b-b6bf-c58004a34ee3"
  43. :block/name "foo"}},
  44. {:block/content "foo content b",
  45. :block/format :markdown
  46. :block/parent {:block/uuid #uuid "71515b7d-b5fc-496b-b6bf-c58004a34ee3"
  47. :block/name "foo"}}),
  48. :pages
  49. ({:block/format :markdown,
  50. :block/name "bar"
  51. :block/original-name "Bar"
  52. :block/uuid #uuid "71515b7d-b5fc-496b-b6bf-c58004a34ee3"})})
  53. (deftest parse-file
  54. (testing "id properties"
  55. (let [conn (gp-db/start-conn)]
  56. (graph-parser/parse-file conn "foo.md" "- id:: 628953c1-8d75-49fe-a648-f4c612109098" {})
  57. (is (= [{:id "628953c1-8d75-49fe-a648-f4c612109098"}]
  58. (->> (d/q '[:find (pull ?b [*])
  59. :in $
  60. :where [?b :block/content] [(missing? $ ?b :block/name)]]
  61. @conn)
  62. (map first)
  63. (map :block/properties)))
  64. "id as text has correct :block/properties")))
  65. (testing "unexpected failure during block extraction"
  66. (let [conn (gp-db/start-conn)
  67. deleted-page (atom nil)]
  68. (with-redefs [gp-block/with-pre-block-if-exists (fn stub-failure [& _args]
  69. (throw (js/Error "Testing unexpected failure")))]
  70. (try
  71. (graph-parser/parse-file conn "foo.md" "- id:: 628953c1-8d75-49fe-a648-f4c612109098"
  72. {:delete-blocks-fn (fn [_db page _file _uuids]
  73. (reset! deleted-page page))})
  74. (catch :default _)))
  75. (is (= nil @deleted-page)
  76. "Page should not be deleted when there is unexpected failure")))
  77. (testing "parsing whiteboard page"
  78. (let [conn (gp-db/start-conn)]
  79. (graph-parser/parse-file conn "/whiteboards/foo.edn" (pr-str foo-edn) {})
  80. (let [blocks (d/q '[:find (pull ?b [* {:block/page
  81. [:block/name
  82. :block/original-name
  83. :block/type
  84. {:block/file
  85. [:file/path]}]}])
  86. :in $
  87. :where [?b :block/content] [(missing? $ ?b :block/name)]]
  88. @conn)
  89. parent (:block/page (ffirst blocks))]
  90. (is (= {:block/name "foo"
  91. :block/original-name "Foo"
  92. :block/type ["whiteboard"]
  93. :block/file {:file/path "/whiteboards/foo.edn"}}
  94. parent)
  95. "parsed block in the whiteboard page has correct parent page"))))
  96. (testing "Loading whiteboard pages that same block/uuid should throw an error."
  97. (let [conn (gp-db/start-conn)]
  98. (graph-parser/parse-file conn "/whiteboards/foo.edn" (pr-str foo-edn) {})
  99. (is (thrown-with-msg?
  100. js/Error
  101. #"Conflicting upserts"
  102. (graph-parser/parse-file conn "/whiteboards/foo-conflict.edn" (pr-str foo-conflict-edn) {})))))
  103. (testing "Loading whiteboard pages should ignore the :block/name property inside :block/parent."
  104. (let [conn (gp-db/start-conn)]
  105. (graph-parser/parse-file conn "/whiteboards/foo.edn" (pr-str foo-edn) {})
  106. (graph-parser/parse-file conn "/whiteboards/bar.edn" (pr-str bar-edn) {})
  107. (let [pages (d/q '[:find ?name
  108. :in $
  109. :where
  110. [?b :block/name ?name]
  111. [?b :block/type "whiteboard"]]
  112. @conn)]
  113. (is (= pages #{["foo"] ["bar"]}))))))
  114. (defn- test-property-order [num-properties]
  115. (let [conn (gp-db/start-conn)
  116. properties (mapv #(keyword (str "p" %)) (range 0 num-properties))
  117. text (->> properties
  118. (map #(str (name %) ":: " (name %) "-value"))
  119. (string/join "\n"))
  120. ;; Test page properties and block properties
  121. body (str text "\n- " text)
  122. _ (graph-parser/parse-file conn "foo.md" body {})
  123. properties-orders (->> (d/q '[:find (pull ?b [*])
  124. :in $
  125. :where [?b :block/content] [(missing? $ ?b :block/name)]]
  126. @conn)
  127. (map first)
  128. (map :block/properties-order))]
  129. (is (every? vector? properties-orders)
  130. "Order is persisted as a vec to avoid edn serialization quirks")
  131. (is (= [properties properties] properties-orders)
  132. "Property order")))
  133. (deftest properties-order
  134. (testing "Sort order and persistence of a few properties"
  135. (test-property-order 4))
  136. (testing "Sort order and persistence of 10 properties"
  137. (test-property-order 10)))
  138. (deftest quoted-property-values
  139. (let [conn (gp-db/start-conn)
  140. _ (graph-parser/parse-file conn
  141. "foo.md"
  142. "- desc:: \"#foo is not a ref\""
  143. {:extract-options {:user-config {}}})
  144. block (->> (d/q '[:find (pull ?b [* {:block/refs [*]}])
  145. :in $
  146. :where [?b :block/properties]]
  147. @conn)
  148. (map first)
  149. first)]
  150. (is (= {:desc "\"#foo is not a ref\""}
  151. (:block/properties block))
  152. "Quoted value is unparsed")
  153. (is (= ["desc"]
  154. (map :block/original-name (:block/refs block)))
  155. "No refs from property value")))
  156. (deftest non-string-property-values
  157. (let [conn (gp-db/start-conn)]
  158. (graph-parser/parse-file conn
  159. "lythe-of-heaven.md"
  160. "rating:: 8\nrecommend:: true\narchive:: false"
  161. {})
  162. (is (= {:rating 8 :recommend true :archive false}
  163. (->> (d/q '[:find (pull ?b [*])
  164. :in $
  165. :where [?b :block/properties]]
  166. @conn)
  167. (map (comp :block/properties first))
  168. first)))))
  169. (deftest linkable-built-in-properties
  170. (let [conn (gp-db/start-conn)
  171. _ (graph-parser/parse-file conn
  172. "lol.md"
  173. (str "alias:: 233\ntags:: fun, facts"
  174. "\n- "
  175. "alias:: 666\ntags:: block, facts")
  176. {})
  177. page-block (->> (d/q '[:find (pull ?b [:block/properties {:block/alias [:block/name]} {:block/tags [:block/name]}])
  178. :in $
  179. :where [?b :block/name "lol"]]
  180. @conn)
  181. (map first)
  182. first)
  183. block (->> (d/q '[:find (pull ?b [:block/properties])
  184. :in $
  185. :where
  186. [?b :block/properties]
  187. [(missing? $ ?b :block/pre-block?)]
  188. [(missing? $ ?b :block/name)]]
  189. @conn)
  190. (map first)
  191. first)]
  192. (is (= {:block/alias [{:block/name "233"}]
  193. :block/tags [{:block/name "fun"} {:block/name "facts"}]
  194. :block/properties {:alias #{"233"} :tags #{"fun" "facts"}}}
  195. page-block)
  196. "page properties, alias and tags are correct")
  197. (is (every? set? (vals (:block/properties page-block)))
  198. "Linked built-in property values as sets provides for easier transforms")
  199. (is (= {:block/properties {:alias #{"666"} :tags #{"block" "facts"}}}
  200. block)
  201. "block properties are correct")))
  202. (defn- property-relationships-test
  203. "Runs tests on page properties and block properties. file-properties is what is
  204. visible in a file and db-properties is what is pulled out from the db"
  205. [file-properties db-properties user-config]
  206. (let [conn (gp-db/start-conn)
  207. page-content (gp-property/->block-content file-properties)
  208. ;; Create Block properties from given page ones
  209. block-property-transform (fn [m] (update-keys m #(keyword (str "block-" (name %)))))
  210. block-file-properties (block-property-transform file-properties)
  211. block-content (gp-property/->block-content block-file-properties)
  212. _ (graph-parser/parse-file conn
  213. "property-relationships.md"
  214. (str page-content "\n- " block-content)
  215. {:extract-options {:user-config user-config}})
  216. pages (->> (d/q '[:find (pull ?b [* :block/properties])
  217. :in $
  218. :where [?b :block/name] [?b :block/properties]]
  219. @conn)
  220. (map first))
  221. _ (assert (= 1 (count pages)))
  222. blocks (->> (d/q '[:find (pull ?b [:block/pre-block?
  223. :block/properties
  224. :block/properties-text-values
  225. {:block/refs [:block/original-name]}])
  226. :in $
  227. :where [?b :block/properties] [(missing? $ ?b :block/name)]]
  228. @conn)
  229. (map first)
  230. (map (fn [m] (update m :block/refs #(map :block/original-name %)))))
  231. block-db-properties (block-property-transform db-properties)]
  232. (testing "Page properties"
  233. (is (= db-properties (:block/properties (first pages)))
  234. "page has expected properties")
  235. (is (= file-properties (:block/properties-text-values (first pages)))
  236. "page has expected full text of properties"))
  237. (testing "Pre-block and block properties"
  238. (is (= [true nil] (map :block/pre-block? blocks))
  239. "page has 2 blocks, one of which is a pre-block")
  240. (is (= [db-properties block-db-properties]
  241. (map :block/properties blocks))
  242. "pre-block/page and block have expected properties")
  243. (is (= [file-properties block-file-properties]
  244. (map :block/properties-text-values blocks))
  245. "pre-block/page and block have expected full text of properties")
  246. ;; has expected refs
  247. (are [db-props refs]
  248. (= (->> (vals db-props)
  249. ;; ignore string values
  250. (mapcat #(if (coll? %) % []))
  251. (concat (map name (keys db-props)))
  252. set)
  253. (set refs))
  254. ; pre-block/page has expected refs
  255. db-properties (first (map :block/refs blocks))
  256. ;; block has expected refs
  257. block-db-properties (second (map :block/refs blocks))))))
  258. (deftest property-relationships
  259. (let [properties {:single-link "[[bar]]"
  260. :multi-link "[[Logseq]] is the fastest #triples #[[text editor]]"
  261. :desc "This is a multiple sentence description. It has one [[link]]"
  262. :comma-prop "one, two,three"}]
  263. (property-relationships-test
  264. properties
  265. {:single-link #{"bar"}
  266. :multi-link #{"Logseq" "triples" "text editor"}
  267. :desc #{"link"}
  268. :comma-prop "one, two,three"}
  269. {})))
  270. (deftest invalid-properties
  271. (let [conn (gp-db/start-conn)
  272. properties {"foo" "valid"
  273. "[[foo]]" "invalid"
  274. "some,prop" "invalid"
  275. "#blarg" "invalid"}
  276. body (str (gp-property/->block-content properties)
  277. "\n- " (gp-property/->block-content properties))]
  278. (graph-parser/parse-file conn "foo.md" body {})
  279. (is (= [{:block/properties {:foo "valid"}
  280. :block/invalid-properties #{"[[foo]]" "some,prop" "#blarg"}}]
  281. (->> (d/q '[:find (pull ?b [*])
  282. :in $
  283. :where
  284. [?b :block/properties]
  285. [(missing? $ ?b :block/pre-block?)]
  286. [(missing? $ ?b :block/name)]]
  287. @conn)
  288. (map first)
  289. (map #(select-keys % [:block/properties :block/invalid-properties]))))
  290. "Has correct (in)valid block properties")
  291. (is (= [{:block/properties {:foo "valid"}
  292. :block/invalid-properties #{"[[foo]]" "some,prop" "#blarg"}}]
  293. (->> (d/q '[:find (pull ?b [*])
  294. :in $
  295. :where [?b :block/properties] [?b :block/name]]
  296. @conn)
  297. (map first)
  298. (map #(select-keys % [:block/properties :block/invalid-properties]))))
  299. "Has correct (in)valid page properties")))
  300. (deftest correct-page-names-created-from-title
  301. (testing "from title"
  302. (let [conn (gp-db/start-conn)
  303. built-in-pages (set (map string/lower-case gp-db/built-in-pages-names))]
  304. (graph-parser/parse-file conn
  305. "foo.md"
  306. "title:: core.async"
  307. {})
  308. (is (= #{"core.async"}
  309. (->> (d/q '[:find (pull ?b [*])
  310. :in $
  311. :where [?b :block/name]]
  312. @conn)
  313. (map (comp :block/name first))
  314. (remove built-in-pages)
  315. set)))))
  316. (testing "from cased org title"
  317. (let [conn (gp-db/start-conn)
  318. built-in-pages (set gp-db/built-in-pages-names)]
  319. (graph-parser/parse-file conn
  320. "foo.org"
  321. ":PROPERTIES:
  322. :ID: 72289d9a-eb2f-427b-ad97-b605a4b8c59b
  323. :END:
  324. #+tItLe: Well parsed!"
  325. {})
  326. (is (= #{"Well parsed!"}
  327. (->> (d/q '[:find (pull ?b [*])
  328. :in $
  329. :where [?b :block/name]]
  330. @conn)
  331. (map (comp :block/original-name first))
  332. (remove built-in-pages)
  333. set))))))
  334. (deftest correct-page-names-created-from-page-refs
  335. (testing "for file, mailto, web and other uris in markdown"
  336. (let [conn (gp-db/start-conn)
  337. built-in-pages (set (map string/lower-case gp-db/built-in-pages-names))]
  338. (graph-parser/parse-file conn
  339. "foo.md"
  340. (str "- [title]([[bar]])\n"
  341. ;; all of the uris below do not create pages
  342. "- ![image.png](../assets/image_1630480711363_0.png)\n"
  343. "- [Filename.txt](file:///E:/test/Filename.txt)\n"
  344. "- [mail](mailto:[email protected]?subject=TestSubject)\n"
  345. "- [onenote link](onenote:https://d.docs.live.net/b2127346582e6386a/blablabla/blablabla/blablabla%20blablabla.one#Etat%202019&section-id={133DDF16-9A1F-4815-9A05-44303784442E6F94}&page-id={3AAB677F0B-328F-41D0-AFF5-66408819C085}&end)\n"
  346. "- [lock file](deps/graph-parser/yarn.lock)"
  347. "- [example](https://example.com)"))
  348. (is (= #{"foo" "bar"}
  349. (->> (d/q '[:find (pull ?b [*])
  350. :in $
  351. :where [?b :block/name]]
  352. @conn)
  353. (map (comp :block/name first))
  354. (remove built-in-pages)
  355. set)))))
  356. (testing "for web and page uris in org"
  357. (let [conn (gp-db/start-conn)
  358. built-in-pages (set (map string/lower-case gp-db/built-in-pages-names))]
  359. (graph-parser/parse-file conn
  360. "foo.org"
  361. (str "* [[bar][title]]\n"
  362. ;; all of the uris below do not create pages
  363. "* [[https://example.com][example]]\n"
  364. "* [[../assets/conga_parrot.gif][conga]]"))
  365. (is (= #{"foo" "bar"}
  366. (->> (d/q '[:find (pull ?b [*])
  367. :in $
  368. :where [?b :block/name]]
  369. @conn)
  370. (map (comp :block/name first))
  371. (remove built-in-pages)
  372. set))))))
  373. (deftest duplicated-ids
  374. (testing "duplicated block ids in same file"
  375. (let [conn (gp-db/start-conn)
  376. extract-block-ids (atom #{})
  377. parse-opts {:extract-options {:extract-block-ids extract-block-ids}}
  378. block-id #uuid "63f199bc-c737-459f-983d-84acfcda14fe"]
  379. (graph-parser/parse-file conn
  380. "foo.md"
  381. "- foo
  382. id:: 63f199bc-c737-459f-983d-84acfcda14fe
  383. - bar
  384. id:: 63f199bc-c737-459f-983d-84acfcda14fe
  385. "
  386. parse-opts)
  387. (let [blocks (:block/_parent (ldb/get-page @conn "foo"))]
  388. (is (= 2 (count blocks)))
  389. (is (= 1 (count (filter #(= (:block/uuid %) block-id) blocks)))))))
  390. (testing "duplicated block ids in multiple files"
  391. (let [conn (gp-db/start-conn)
  392. extract-block-ids (atom #{})
  393. parse-opts {:extract-options {:extract-block-ids extract-block-ids}}
  394. block-id #uuid "63f199bc-c737-459f-983d-84acfcda14fe"]
  395. (graph-parser/parse-file conn
  396. "foo.md"
  397. "- foo
  398. id:: 63f199bc-c737-459f-983d-84acfcda14fe
  399. bar
  400. - test"
  401. parse-opts)
  402. (graph-parser/parse-file conn
  403. "bar.md"
  404. "- bar
  405. id:: 63f199bc-c737-459f-983d-84acfcda14fe
  406. bar
  407. - test
  408. "
  409. parse-opts)
  410. (is (= "foo"
  411. (-> (d/entity @conn [:block/uuid block-id])
  412. :block/page
  413. :block/name)))
  414. (let [bar-block (first (:block/_parent (ldb/get-page @conn "bar")))]
  415. (is (some? (:block/uuid bar-block)))
  416. (is (not= (:block/uuid bar-block) block-id))))))