Repository: MatthewFlamm/pytest-homeassistant-custom-component (language: python)

Issue:
load_fixture looking for fixtures file within site-packages/pytest_homeassistant_custom_component/fixtures
I import load_fixture from  pytest_homeassistant_custom_component.common:

`from pytest_homeassistant_custom_component.common import MockConfigEntry, load_fixture`

I then try to use it with following code:
`devices_fixture = "devices.json"`
`devices_json = load_fixture(devices_fixture)`

I created a fixtures folder within my tests folder in which I put devices.json file.

When running the test using pytest I get the error that the file is not found. 
For filename it was trying to load it shows: 
`venv/lib/python3.8/site-packages/pytest_homeassistant_custom_component/fixtures/devices.json`

Looking at the code it does:
`path = os.path.join(os.path.dirname(__file__), "fixtures", filename)`

For HASS, the method is located in tests.common; thus dirname would be tests and thus with the join it would be tests\fixtures\filename. 
But here the method ends up being located in venv/lib/python3.8/site-packages/pytest_homeassistant_custom_component instead.





Discussion / hints:
Thanks for the report and the detailed information. 
This is indeed an issue caused on the separate nature of the testing helpers and the tests themselves. 
In addition to `load_fixture`, the following locations also use `__file__` and likely won't work as intended:

* `load_fixture`
* `get_test_config_dir`
* `MockModule`
*  `MockPlatform`

The first is definitely usable in custom component testing.  I doubt the last two would be used in custom components and I'm not sure about the second.
I did find a way how to get path of current test script being executed, but I could not get it to work. My knowledge on this part of limited and clearly being doing something wrong.

It is using py test fixture and request.fspath. See:https://stackoverflow.com/questions/34504757/get-pytest-to-look-within-the-base-directory-of-the-testing-script

Not sure if it might even just be a matter in leveraging that to then allow all 4 easily to be fixed? Otherwise I would say that load_fixture probably is the most used one of the 4. :-)
I think there are two straightforward ways to proceed here. I'm leaning towards the second option, but I need to think about what is the best implementation.

1. You can place your own `load_fixture` function (just copy it directly from `common.py` into a `common.py` module (or whatever you want) inside your `tests` directory.  Then, you can put your file inside `tests/fixtures/`, since the new `load_fixture` function is inside of your `tests` folder.  I think this should work.
2. This library could either override the default `load_fixture` function or provide a new function to provide a way to pass in the absolute path of the testing function.  So instead of doing `load_fixture("file.json")` you would do `load_fixture("file.json", __file__)`.  Then, since `__file__` is provided from the test module itself, we get the correct path.
> It is using py test fixture and request.fspath.

I'm not quite sure about this, but since `load_fixture` is just a function, not a fixture, that is imported, I don't think we can use `request` fixture to fix this.  I think it still requires changing `load_fixture` itself.
Personally I like option 2 then. :-)
I've thought of a third option, and it is more elegant and preserve the current usage.  `traceback.extract_stack` might be able to extract the path to the test module or other module that calls `load_fixture`.  Then it is a like for like usage with the built-in tests, which is a goal of this library.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50daa41..5bfed91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
 # Changelog
 This changelog only includes changes directly related to the structure of this project. Changes in testing behavior may still occur from changes in homeassistant/core.
 
+Changes to minor version indicate a change structurally in this pacakge.  Changes in patch indicate changes solely from homeassistant/core. The latter does not imply no breaking changes are introduced.
+
+## 0.2.0
+* fix `load_fixture`
+
 ## 0.1.0
 * remove Python 3.7 and add Python 3.9
 * remove `async_test`
diff --git a/README.md b/README.md
index 7a0a0be..796c5e5 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,14 @@ The goal is to provide the same functionality as the tests in home-assistant/cor
   * home-assistant/core native test: `from tests.common import MockConfigEntry`
   * custom component test: `from pytest_homeassistant_custom_component.common import MockConfigEntry`
 * If your integration is inside a `custom_components` folder, a `custom_components/__init__.py` file or changes to `sys.path` may be required.
+* If using `load_fixture`, the files need to be in a `fixtures` folder colocated with the tests. For example, a test in `test_sensor.py` can load data from `some_data.json` using `load_fixture` from this structure:
+
+```
+tests/
+   fixtures/
+      some_data.json
+   test_sensor.py
+```
 
 ## Examples:
 * See [list of custom components](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/network/dependents) as examples that use this package.
diff --git a/generate_package.py b/generate_package.py
index 78cd4ee..6d3ec2f 100755
--- a/generate_package.py
+++ b/generate_package.py
@@ -129,3 +129,20 @@
     new_file.write("".join(data))
 
 print(f"Version: {__version__}")
+
+
+# modify load_fixture
+with open(os.path.join(PACKAGE_DIR, "common.py"), "r") as original_file:
+    data = original_file.readlines()
+
+import_time_lineno = [i for i, line in enumerate(data) if "from time" in line]
+assert len(import_time_lineno) == 1
+data.insert(import_time_lineno[0] + 1, "import traceback\n")
+
+load_fixture_lineno = [i for i, line in enumerate(data) if "load_fixture" in line]
+assert len(load_fixture_lineno) == 1
+data.insert(load_fixture_lineno[0] + 2, "    start_path = traceback.extract_stack()[-2].filename\n")
+data[load_fixture_lineno[0] + 3] = data[load_fixture_lineno[0] + 3].replace("__file__", "start_path")
+
+with open(os.path.join(PACKAGE_DIR, "common.py"), "w") as new_file:
+    new_file.writelines(data)


Test patch:
diff --git a/pytest_homeassistant_custom_component/common.py b/pytest_homeassistant_custom_component/common.py
index e769198..b556a9b 100644
--- a/pytest_homeassistant_custom_component/common.py
+++ b/pytest_homeassistant_custom_component/common.py
@@ -17,6 +17,7 @@
 import threading
 import time
 from time import monotonic
+import traceback
 import types
 from typing import Any, Awaitable, Collection, Optional
 from unittest.mock import AsyncMock, Mock, patch
@@ -399,7 +400,8 @@ def async_fire_service_discovered(hass, service, info):
 
 def load_fixture(filename):
     """Load a fixture."""
-    path = os.path.join(os.path.dirname(__file__), "fixtures", filename)
+    start_path = traceback.extract_stack()[-2].filename
+    path = os.path.join(os.path.dirname(start_path), "fixtures", filename)
     with open(path, encoding="utf-8") as fptr:
         return fptr.read()
 
diff --git a/tests/fixtures/test_data.json b/tests/fixtures/test_data.json
new file mode 100644
index 0000000..b676cf6
--- /dev/null
+++ b/tests/fixtures/test_data.json
@@ -0,0 +1,3 @@
+{
+    "test_key": "test_value"
+}
diff --git a/tests/test_common.py b/tests/test_common.py
new file mode 100644
index 0000000..4d9f7b3
--- /dev/null
+++ b/tests/test_common.py
@@ -0,0 +1,9 @@
+"""Tests changes to common module."""
+import json
+
+from pytest_homeassistant_custom_component.common import load_fixture
+
+
+def test_load_fixture():
+    data = json.loads(load_fixture("test_data.json"))
+    assert data == {"test_key": "test_value"}