#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Unit tests for the cloudpickle_pickler module."""

# pytype: skip-file

import os
import tempfile
import threading
import types
import unittest
from unittest import mock

from apache_beam.coders import proto2_coder_test_messages_pb2
from apache_beam.internal import cloudpickle_pickler as beam_cloudpickle
from apache_beam.internal import code_object_pickler
from apache_beam.internal import module_test
from apache_beam.internal.cloudpickle_pickler import dumps
from apache_beam.internal.cloudpickle_pickler import loads
from apache_beam.typehints.schemas import LogicalTypeRegistry
from apache_beam.utils import shared

GLOBAL_DICT_REF = module_test.GLOBAL_DICT


# Allow weakref to dict
class DictWrapper(dict):
  pass


MAIN_MODULE_DICT = DictWrapper()


def acquire_dict():
  return DictWrapper()


class PicklerTest(unittest.TestCase):

  NO_MAPPINGPROXYTYPE = not hasattr(types, "MappingProxyType")

  def test_globals_main_are_pickled_by_value(self):
    self.assertIsNot(MAIN_MODULE_DICT, loads(dumps(lambda: MAIN_MODULE_DICT))())

  def test_globals_shared_are_pickled_by_reference(self):
    shared_handler = shared.Shared()
    original_dict = shared_handler.acquire(acquire_dict)

    unpickled_dict = loads(
        dumps(lambda: shared_handler.acquire(acquire_dict)))()

    self.assertIs(original_dict, unpickled_dict)

  def test_module_globals_are_pickled_by_value_when_directly_referenced(self):
    global_dict = loads(dumps(module_test.GLOBAL_DICT))

    self.assertIsNot(module_test.GLOBAL_DICT, global_dict)

  def test_function_main_with_explicit_module_reference_pickles_by_reference(
      self):
    def returns_global_dict():
      return module_test.GLOBAL_DICT

    self.assertIs(module_test.GLOBAL_DICT, loads(dumps(returns_global_dict))())

  def test_function_main_with_indirect_module_reference_pickles_by_value(self):
    def returns_global_dict():
      return GLOBAL_DICT_REF

    self.assertIsNot(
        module_test.GLOBAL_DICT, loads(dumps(returns_global_dict))())

  def test_function_referencing_unpicklable_object_works_when_imported(self):
    self.assertEqual(
        module_test.UNPICKLABLE_INSTANCE,
        loads(dumps(module_test.fn_returns_unpicklable))())

  def test_closure_with_unpicklable_object_fails_when_imported(self):
    # The entire closure is pickled by value, and therefore module_test is
    # not imported. Requires the module global to be pickled by value.
    with self.assertRaises(Exception):
      loads(dumps(module_test.closure_contains_unpicklable()))

  def test_closure_with_explicit_self_import_can_reference_unpicklable_objects(
      self):
    # The closure imports module_test within the function definition
    # and returns self.UNPICKLABLE_INSTANCE. This allows cloudpickle
    # to use submimort to reference module_test.UNPICKLABLE_INSTANCE
    self.assertIs(
        module_test.UNPICKLABLE_INSTANCE,
        loads(dumps(module_test.closure_contains_unpicklable_imports_self()))())

  def test_closure_main_can_reference_unpicklable_module_objects(self):
    def outer():
      def inner():
        return module_test.UNPICKLABLE_INSTANCE

      return inner

    # Uses subimport to reference module_test.UNPICKLABLE_INSTANCE rather than
    # recreate.
    self.assertIs(module_test.UNPICKLABLE_INSTANCE, loads(dumps(outer()))())

  def test_pickle_nested_enum_descriptor(self):
    NestedEnum = proto2_coder_test_messages_pb2.MessageD.NestedEnum

    def fn():
      return NestedEnum.TWO

    self.assertEqual(fn(), loads(dumps(fn))())

  def test_pickle_top_level_enum_descriptor(self):
    TopLevelEnum = proto2_coder_test_messages_pb2.TopLevelEnum

    def fn():
      return TopLevelEnum.ONE

    self.assertEqual(fn(), loads(dumps(fn))())

  def test_basics(self):
    self.assertEqual([1, 'a', ('z', )], loads(dumps([1, 'a', ('z', )])))
    fun = lambda x: 'xyz-%s' % x
    self.assertEqual('xyz-abc', loads(dumps(fun))('abc'))

  def test_lambda_with_globals(self):
    """Tests that the globals of a function are preserved."""

    # The point of the test is that the lambda being called after unpickling
    # relies on having the re module being loaded.
    self.assertEqual(['abc', 'def'],
                     loads(dumps(
                         module_test.get_lambda_with_globals()))('abc def'))

  def test_lambda_with_main_globals(self):
    self.assertEqual(unittest, loads(dumps(lambda: unittest))())

  def test_lambda_with_closure(self):
    """Tests that the closure of a function is preserved."""
    self.assertEqual(
        'closure: abc',
        loads(dumps(module_test.get_lambda_with_closure('abc')))())

  def test_class_object_pickled(self):
    self.assertEqual(['abc', 'def'],
                     loads(dumps(module_test.Xyz))().foo('abc def'))

  def test_class_instance_pickled(self):
    self.assertEqual(['abc', 'def'],
                     loads(dumps(module_test.XYZ_OBJECT)).foo('abc def'))

  def test_pickling_preserves_closure_of_a_function(self):
    self.assertEqual(
        'X:abc', loads(dumps(module_test.TopClass.NestedClass('abc'))).datum)
    self.assertEqual(
        'Y:abc',
        loads(dumps(module_test.TopClass.MiddleClass.NestedClass('abc'))).datum)

  def test_pickle_dynamic_class(self):
    self.assertEqual(
        'Z:abc', loads(dumps(module_test.create_class('abc'))).get())

  def test_generators(self):
    with self.assertRaises(TypeError):
      dumps((_ for _ in range(10)))

  def test_recursive_class(self):
    self.assertEqual(
        'RecursiveClass:abc',
        loads(dumps(module_test.RecursiveClass('abc').datum)))

  def test_function_with_external_reference(self):
    out_of_scope_var = 'expected_value'

    def foo():
      return out_of_scope_var

    self.assertEqual('expected_value', loads(dumps(foo))())

  def test_pickle_rlock(self):
    rlock_instance = threading.RLock()
    rlock_type = type(rlock_instance)

    self.assertIsInstance(loads(dumps(rlock_instance)), rlock_type)

  def test_pickle_lock(self):
    lock_instance = threading.Lock()
    lock_type = type(lock_instance)

    self.assertIsInstance(loads(dumps(lock_instance)), lock_type)

  @unittest.skipIf(NO_MAPPINGPROXYTYPE, 'test if MappingProxyType introduced')
  def test_dump_and_load_mapping_proxy(self):
    self.assertEqual(
        'def', loads(dumps(types.MappingProxyType({'abc': 'def'})))['abc'])
    self.assertEqual(
        types.MappingProxyType, type(loads(dumps(types.MappingProxyType({})))))

  # pylint: disable=exec-used
  def test_dataclass(self):
    exec(
        '''
from apache_beam.internal.module_test import DataClass
self.assertEqual(DataClass(datum='abc'), loads(dumps(DataClass(datum='abc'))))
    ''')

  def test_best_effort_determinism_not_implemented(self):
    with self.assertLogs('apache_beam.internal.cloudpickle_pickler',
                         "WARNING") as l:
      dumps(123, enable_best_effort_determinism=True)
      self.assertIn(
          'Ignoring unsupported option: enable_best_effort_determinism',
          '\n'.join(l.output))

  @mock.patch.object(
      beam_cloudpickle.DEFAULT_CONFIG, 'filepath_interceptor', autospec=True)
  def test_default_config_interceptor(self, mock_filepath_interceptor):
    """Tests config.filepath_interceptor is called for CodeType pickling."""
    mock_filepath_interceptor.side_effect = (
        code_object_pickler.get_normalized_path)

    def sample_func():
      return "Beam"

    code_obj = sample_func.__code__
    original_filename = os.path.abspath(code_obj.co_filename)
    pickled_code = beam_cloudpickle.dumps(code_obj)
    unpickled_code = beam_cloudpickle.loads(pickled_code)

    mock_filepath_interceptor.assert_called()

    unpickled_filename = os.path.abspath(unpickled_code.co_filename)
    self.assertEqual(unpickled_filename, original_filename)

  @mock.patch(
      "apache_beam.coders.typecoders.registry.load_custom_type_coder_tuples")
  @mock.patch(
      "apache_beam.typehints.schemas.LogicalType._known_logical_types.load")
  def test_dump_load_session(self, logicaltype_mock, coder_mock):
    session_file = 'pickled'

    with tempfile.TemporaryDirectory() as tmp_dirname:
      pickled_session_file = os.path.join(tmp_dirname, session_file)
      beam_cloudpickle.dump_session(pickled_session_file)
      beam_cloudpickle.load_session(pickled_session_file)
    load_logical_types = logicaltype_mock.call_args.args
    load_coders = coder_mock.call_args.args
    self.assertEqual(len(load_logical_types), 1)
    self.assertEqual(len(load_coders), 1)
    self.assertTrue(isinstance(load_logical_types[0], LogicalTypeRegistry))
    self.assertTrue(isinstance(load_coders[0], list))


if __name__ == '__main__':
  unittest.main()
