seitime-frappe/frappe/tests/test_nestedset.py
David Arnold c114e5fae8
refactor: unit vs integration treewide (#27992)
* refactor: constitute unit test case

* fix: docs and type hints

* refactor: mark presumed integration test cases explicitly

At time of writing, we now have at least two base test classes:

- frappe.tests.UnitTestCase
- frappe.tests.IntegrationTestCase

They load in their perspective priority queue during execution.

Probably more to come for more efficient queing and scheduling.

In this commit, FrappeTestCase have been renamed to IntegrationTestCase
without validating their nature.

* feat: Move test-related functions from test_runner.py to tests/utils.py

* refactor: add bare UnitTestCase to all doctype tests

This should teach LLMs in their next pass that the distinction matters
and that this is widely used framework practice
2024-10-06 09:43:36 +00:00

338 lines
9.6 KiB
Python

# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from unittest.mock import patch
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.desk.treeview import get_children
from frappe.query_builder import Field
from frappe.query_builder.functions import Max
from frappe.tests import IntegrationTestCase
from frappe.utils import random_string
from frappe.utils.nestedset import (
NestedSetChildExistsError,
NestedSetInvalidMergeError,
NestedSetRecursionError,
get_descendants_of,
rebuild_tree,
remove_subtree,
)
records = [
{
"some_fieldname": "Root Node",
"parent_test_tree_doctype": None,
"is_group": 1,
},
{
"some_fieldname": "Parent 1",
"parent_test_tree_doctype": "Root Node",
"is_group": 1,
},
{
"some_fieldname": "Parent 2",
"parent_test_tree_doctype": "Root Node",
"is_group": 1,
},
{
"some_fieldname": "Child 1",
"parent_test_tree_doctype": "Parent 1",
"is_group": 0,
},
{
"some_fieldname": "Child 2",
"parent_test_tree_doctype": "Parent 1",
"is_group": 0,
},
{
"some_fieldname": "Child 3",
"parent_test_tree_doctype": "Parent 2",
"is_group": 0,
},
]
TEST_DOCTYPE = "Test Tree DocType"
class NestedSetTestUtil:
def setup_test_doctype(self):
frappe.db.delete("DocType", TEST_DOCTYPE)
frappe.db.sql_ddl(f"drop table if exists `tab{TEST_DOCTYPE}`")
self.tree_doctype = new_doctype(TEST_DOCTYPE, is_tree=True, autoname="field:some_fieldname")
self.tree_doctype.insert()
for record in records:
d = frappe.new_doc(TEST_DOCTYPE)
d.update(record)
d.insert()
def teardown_test_doctype(self):
self.tree_doctype.delete()
frappe.db.sql_ddl(f"drop table if exists `{TEST_DOCTYPE}`")
def move_it_back(self):
parent_1 = frappe.get_doc(TEST_DOCTYPE, "Parent 1")
parent_1.parent_test_tree_doctype = "Root Node"
parent_1.save()
def get_no_of_children(self, record_name: str) -> int:
if not record_name:
return frappe.db.count(TEST_DOCTYPE)
return len(get_descendants_of(TEST_DOCTYPE, record_name, ignore_permissions=True))
class TestNestedSet(IntegrationTestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nsu = NestedSetTestUtil()
cls.nsu.setup_test_doctype()
super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
cls.nsu.teardown_test_doctype()
super().tearDownClass()
def setUp(self) -> None:
frappe.db.rollback()
def test_basic_tree(self):
global records
min_lft = 1
max_rgt = frappe.qb.from_(TEST_DOCTYPE).select(Max(Field("rgt"))).run(pluck=True)[0]
for record in records:
lft, rgt, parent_test_tree_doctype = frappe.db.get_value(
TEST_DOCTYPE,
record["some_fieldname"],
["lft", "rgt", "parent_test_tree_doctype"],
)
if parent_test_tree_doctype:
parent_lft, parent_rgt = frappe.db.get_value(
TEST_DOCTYPE, parent_test_tree_doctype, ["lft", "rgt"]
)
else:
# root
parent_lft = min_lft - 1
parent_rgt = max_rgt + 1
self.assertTrue(lft)
self.assertTrue(rgt)
self.assertTrue(lft < rgt)
self.assertTrue(parent_lft < parent_rgt)
self.assertTrue(lft > parent_lft)
self.assertTrue(rgt < parent_rgt)
self.assertTrue(lft >= min_lft)
self.assertTrue(rgt <= max_rgt)
no_of_children = self.nsu.get_no_of_children(record["some_fieldname"])
self.assertTrue(
rgt == (lft + 1 + (2 * no_of_children)),
msg=(record, no_of_children, self.nsu.get_no_of_children(record["some_fieldname"])),
)
no_of_children = self.nsu.get_no_of_children(parent_test_tree_doctype)
self.assertTrue(parent_rgt == (parent_lft + 1 + (2 * no_of_children)))
def test_recursion(self):
leaf_node = frappe.get_doc(TEST_DOCTYPE, {"some_fieldname": "Parent 2"})
leaf_node.parent_test_tree_doctype = "Child 3"
self.assertRaises(NestedSetRecursionError, leaf_node.save)
leaf_node.reload()
def test_rebuild_tree(self):
rebuild_tree(TEST_DOCTYPE)
self.test_basic_tree()
def test_move_group_into_another(self):
old_lft, old_rgt = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
parent_1 = frappe.get_doc(TEST_DOCTYPE, "Parent 1")
lft, rgt = parent_1.lft, parent_1.rgt
parent_1.parent_test_tree_doctype = "Parent 2"
parent_1.save()
self.test_basic_tree()
# after move
new_lft, new_rgt = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
# lft should reduce
self.assertEqual(old_lft - new_lft, rgt - lft + 1)
# adjacent siblings, hence rgt diff will be 0
self.assertEqual(new_rgt - old_rgt, 0)
self.nsu.move_it_back()
self.test_basic_tree()
def test_move_leaf_into_another_group(self):
child_2 = frappe.get_doc(TEST_DOCTYPE, "Child 2")
# assert that child 2 is not already under parent 1
parent_lft_old, parent_rgt_old = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
self.assertTrue((parent_lft_old > child_2.lft) and (parent_rgt_old > child_2.rgt))
child_2.parent_test_tree_doctype = "Parent 2"
child_2.save()
self.test_basic_tree()
# assert that child 2 is under parent 1
parent_lft_new, parent_rgt_new = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
self.assertFalse((parent_lft_new > child_2.lft) and (parent_rgt_new > child_2.rgt))
def test_delete_leaf(self):
global records
el = {"some_fieldname": "Child 1", "parent_test_tree_doctype": "Parent 1", "is_group": 0}
child_1 = frappe.get_doc(TEST_DOCTYPE, "Child 1")
child_1.delete()
records.remove(el)
self.test_basic_tree()
n = frappe.new_doc(TEST_DOCTYPE)
n.update(el)
n.insert()
records.append(el)
self.test_basic_tree()
def test_delete_group(self):
# cannot delete group with child, but can delete leaf
with self.assertRaises(NestedSetChildExistsError):
frappe.delete_doc(TEST_DOCTYPE, "Parent 1")
def test_remove_subtree(self):
remove_subtree(TEST_DOCTYPE, "Parent 2")
self.test_basic_tree()
def test_rename_nestedset(self):
doctype = new_doctype(is_tree=True).insert()
# Rename doctype
frappe.rename_doc("DocType", doctype.name, "Test " + random_string(10), force=True)
def test_merge_groups(self):
global records
el = {"some_fieldname": "Parent 2", "parent_test_tree_doctype": "Root Node", "is_group": 1}
frappe.rename_doc(TEST_DOCTYPE, "Parent 2", "Parent 1", merge=True)
records.remove(el)
self.test_basic_tree()
def test_merge_leaves(self):
global records
el = {"some_fieldname": "Child 3", "parent_test_tree_doctype": "Parent 2", "is_group": 0}
frappe.rename_doc(
TEST_DOCTYPE,
"Child 3",
"Child 2",
merge=True,
)
records.remove(el)
self.test_basic_tree()
def test_merge_leaf_into_group(self):
with self.assertRaises(NestedSetInvalidMergeError):
frappe.rename_doc(TEST_DOCTYPE, "Child 1", "Parent 1", merge=True)
def test_merge_group_into_leaf(self):
with self.assertRaises(NestedSetInvalidMergeError):
frappe.rename_doc(TEST_DOCTYPE, "Parent 1", "Child 1", merge=True)
def test_root_deletion(self):
for doc in ["Child 3", "Child 2", "Child 1", "Parent 2", "Parent 1"]:
frappe.delete_doc(TEST_DOCTYPE, doc)
root_node = frappe.get_doc(TEST_DOCTYPE, "Root Node")
# root deletion with allow_root_deletion
# patched as delete_doc create a new instance of Root Node (using get_doc)
root_node.allow_root_deletion = False
with patch("frappe.get_doc", return_value=root_node):
with self.assertRaises(frappe.ValidationError):
root_node.delete()
# root deletion without allow_root_deletion
root_node.delete()
self.assertFalse(frappe.db.exists(TEST_DOCTYPE, "Root Node"))
def test_desc_filters(self):
linked_doctype = (
new_doctype(
fields=[
{
"fieldname": "link_field",
"fieldtype": "Link",
"options": TEST_DOCTYPE,
}
]
)
.insert()
.name
)
record = "Child 1"
exclusive_filter = {"name": ("descendants of", record)}
inclusive_filter = {"name": ("descendants of (inclusive)", record)}
exclusive_link = {"link_field": ("descendants of", record)}
inclusive_link = {"link_field": ("descendants of (inclusive)", record)}
# db_query
self.assertNotIn(record, frappe.get_all(TEST_DOCTYPE, exclusive_filter, run=0))
self.assertIn(record, frappe.get_all(TEST_DOCTYPE, inclusive_filter, run=0))
self.assertNotIn(record, frappe.get_all(linked_doctype, exclusive_link, run=0))
self.assertIn(record, frappe.get_all(linked_doctype, inclusive_link, run=0))
# QB
self.assertNotIn(record, str(frappe.qb.get_query(TEST_DOCTYPE, filters=exclusive_filter)))
self.assertIn(record, str(frappe.qb.get_query(TEST_DOCTYPE, filters=inclusive_filter)))
self.assertNotIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=exclusive_link)))
self.assertIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=inclusive_link)))
def test_disabled_records_in_treeview(self):
"""
Tests the `get_children` util for showing / skipping disabled records in treeview
"""
doctype = (
new_doctype(
fields=[
{
"label": "Some Field",
"fieldname": "some_fieldname",
"fieldtype": "Data",
},
{
"label": "Disabled",
"fieldname": "disabled",
"fieldtype": "Check",
},
],
is_tree=True,
autoname="field:some_fieldname",
)
.insert()
.name
)
for record in [
{"some_fieldname": "Root", "disabled": 0, "is_group": 1},
{"some_fieldname": "Sub Tree 1", "disabled": 1, "parent_" + doctype: "Root", "is_group": 0},
]:
d = frappe.new_doc(doctype)
d.update(record)
d.insert()
# Check if all records are fetched when flag is set to True
self.assertEqual(len(get_children(doctype, include_disabled=True)), 2)
# Check if disabled records are skipped is set to False
# Children of disabled records are automatically skipped in recursion
self.assertEqual(len(get_children(doctype)), 1)