text: # ##### BEGIN GPL LICENSE BLOCK #####
  #
  #  This program is free software; you can redistribute it and/or
  #  modify it under the terms of the GNU General Public License
  #  as published by the Free Software Foundation; either version 2
  #  of the License, or (at your option) any later version.
  #
  #  This program is distributed in the hope that it will be useful,
  #  but WITHOUT ANY WARRANTY; without even the implied warranty of
  #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  #  GNU General Public License for more details.
  #
  #  You should have received a copy of the GNU General Public License
  #  along with this program; if not, write to the Free Software Foundation,
  #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  #
  # ##### END GPL LICENSE BLOCK #####
  
  # <pep8 compliant>
  
  """
  blender -b -noaudio --factory-startup --python tests/python/bl_animation_fcurves.py -- --testdir /path/to/lib/tests/animation
  """
  
  import pathlib
  import sys
  import unittest
  from math import degrees, radians
  from typing import List
  
  import bpy
  
  
  class AbstractAnimationTest:
      @classmethod
      def setUpClass(cls):
          cls.testdir = args.testdir
  
      def setUp(self):
          self.assertTrue(self.testdir.exists(),
                          'Test dir %s should exist' % self.testdir)
  
  class FCurveEvaluationTest(AbstractAnimationTest, unittest.TestCase):
      def test_fcurve_versioning_291(self):
          # See D8752.
          bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "fcurve-versioning-291.blend"))
          cube = bpy.data.objects['Cube']
          fcurve = cube.animation_data.action.fcurves.find('location', index=0)
  
          self.assertAlmostEqual(0.0, fcurve.evaluate(1))
          self.assertAlmostEqual(0.019638698548078537, fcurve.evaluate(2))
          self.assertAlmostEqual(0.0878235399723053, fcurve.evaluate(3))
          self.assertAlmostEqual(0.21927043795585632, fcurve.evaluate(4))
          self.assertAlmostEqual(0.41515052318573, fcurve.evaluate(5))
          self.assertAlmostEqual(0.6332430243492126, fcurve.evaluate(6))
          self.assertAlmostEqual(0.8106040954589844, fcurve.evaluate(7))
          self.assertAlmostEqual(0.924369215965271, fcurve.evaluate(8))
          self.assertAlmostEqual(0.9830065965652466, fcurve.evaluate(9))
          self.assertAlmostEqual(1.0, fcurve.evaluate(10))
  
      def test_fcurve_extreme_handles(self):
          # See D8752.
          bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "fcurve-extreme-handles.blend"))
          cube = bpy.data.objects['Cube']
          fcurve = cube.animation_data.action.fcurves.find('location', index=0)
  
          self.assertAlmostEqual(0.0, fcurve.evaluate(1))
          self.assertAlmostEqual(0.004713400732725859, fcurve.evaluate(2))
          self.assertAlmostEqual(0.022335870191454887, fcurve.evaluate(3))
          self.assertAlmostEqual(0.06331237405538559, fcurve.evaluate(4))
          self.assertAlmostEqual(0.16721539199352264, fcurve.evaluate(5))
          self.assertAlmostEqual(0.8327845335006714, fcurve.evaluate(6))
          self.assertAlmostEqual(0.9366875886917114, fcurve.evaluate(7))
          self.assertAlmostEqual(0.9776642322540283, fcurve.evaluate(8))
          self.assertAlmostEqual(0.9952865839004517, fcurve.evaluate(9))
          self.assertAlmostEqual(1.0, fcurve.evaluate(10))
  
  
  class EulerFilterTest(AbstractAnimationTest, unittest.TestCase):
      def setUp(self):
          super().setUp()
          bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "euler-filter.blend"))
  
      def test_multi_channel_filter(self):
          """Test fixing discontinuities that require all X/Y/Z rotations to work."""
  
          self.activate_object('Three-Channel-Jump')
          fcu_rot = self.active_object_rotation_channels()
  
          ## Check some pre-filter values to make sure the file is as we expect.
          # Keyframes before the "jump". These shouldn't be touched by the filter.
          self.assertEqualAngle(-87.5742, fcu_rot[0], 22)
          self.assertEqualAngle(69.1701, fcu_rot[1], 22)
          self.assertEqualAngle(-92.3918, fcu_rot[2], 22)
          # Keyframes after the "jump". These should be updated by the filter.
          self.assertEqualAngle(81.3266, fcu_rot[0], 23)
          self.assertEqualAngle(111.422, fcu_rot[1], 23)
          self.assertEqualAngle(76.5996, fcu_rot[2], 23)
  
          bpy.ops.graph.euler_filter(self.get_context())
  
          # Keyframes before the "jump". These shouldn't be touched by the filter.
          self.assertEqualAngle(-87.5742, fcu_rot[0], 22)
          self.assertEqualAngle(69.1701, fcu_rot[1], 22)
          self.assertEqualAngle(-92.3918, fcu_rot[2], 22)
          # Keyframes after the "jump". These should be updated by the filter.
          self.assertEqualAngle(-98.6734, fcu_rot[0], 23)
          self.assertEqualAngle(68.5783, fcu_rot[1], 23)
          self.assertEqualAngle(-103.4, fcu_rot[2], 23)
  
      def test_single_channel_filter(self):
          """Test fixing discontinuities in single channels."""
  
          self.activate_object('One-Channel-Jumps')
          fcu_rot = self.active_object_rotation_channels()
  
          ## Check some pre-filter values to make sure the file is as we expect.
          # Keyframes before the "jump". These shouldn't be touched by the filter.
          self.assertEqualAngle(360, fcu_rot[0], 15)
          self.assertEqualAngle(396, fcu_rot[1], 21)  # X and Y are keyed on different frames.
          # Keyframes after the "jump". These should be updated by the filter.
          self.assertEqualAngle(720, fcu_rot[0], 16)
          self.assertEqualAngle(72, fcu_rot[1], 22)
  
          bpy.ops.graph.euler_filter(self.get_context())
  
          # Keyframes before the "jump". These shouldn't be touched by the filter.
          self.assertEqualAngle(360, fcu_rot[0], 15)
          self.assertEqualAngle(396, fcu_rot[1], 21)  # X and Y are keyed on different frames.
          # Keyframes after the "jump". These should be updated by the filter.
          self.assertEqualAngle(360, fcu_rot[0], 16)
          self.assertEqualAngle(432, fcu_rot[1], 22)
  
      def assertEqualAngle(self, angle_degrees: float, fcurve: bpy.types.FCurve, frame: int) -> None:
          self.assertAlmostEqual(
              radians(angle_degrees),
              fcurve.evaluate(frame),
              4,
              "Expected %.3f degrees, but FCurve %s[%d] evaluated to %.3f on frame %d" % (
                  angle_degrees, fcurve.data_path, fcurve.array_index, degrees(fcurve.evaluate(frame)), frame,
              )
          )
  
      @staticmethod
      def get_context():
          ctx = bpy.context.copy()
  
          for area in bpy.context.window.screen.areas:
              if area.type != 'GRAPH_EDITOR':
                  continue
  
              ctx['area'] = area
              ctx['space'] = area.spaces.active
              break
  
          return ctx
  
      @staticmethod
      def activate_object(object_name: str) -> None:
          ob = bpy.data.objects[object_name]
          bpy.context.view_layer.objects.active = ob
  
      @staticmethod
      def active_object_rotation_channels() -> List[bpy.types.FCurve]:
          ob = bpy.context.view_layer.objects.active
          action = ob.animation_data.action
          return [action.fcurves.find('rotation_euler', index=idx) for idx in range(3)]
  
  
  def main():
      global args
      import argparse
  
      if '--' in sys.argv:
          argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
      else:
          argv = sys.argv
  
      parser = argparse.ArgumentParser()
      parser.add_argument('--testdir', required=True, type=pathlib.Path)
      args, remaining = parser.parse_known_args(argv)
  
      unittest.main(argv=remaining)
  
  
  if __name__ == "__main__":
      main()
  