subprocess の各関数を universal_newlines=True で呼び出すとサブプロセスの stdin/stdout/stderr がテキストモードで開かれます。(というか io.TextIOWrapper でラップされます)
locale.getpreferredencoding(False) で得られるコンソールのエンコーディングは固定で、Windows は mbcs、つまり日本語環境では cp932 となります。
ただ一部のコンソールプログラムは mbcs でなく utf-8 で出力したりします。universal_newlines=False のまま呼べばいいだろとかその通りなのですが、次のような回避策を考えてみました。
#!python3
# -*- coding: utf-8 -*-
import sys
import subprocess
from io import TextIOWrapper
import unittest, unittest.mock
def DummyTextIOWrapper(buffer, encoding=None, errors=None, newline=None,
line_buffering=False, write_through=False):
return TextIOWrapper(buffer, encoding or 'utf-8', errors, newline,
line_buffering, write_through)
class SubprocessWithIllegalEncoding(unittest.TestCase):
fail_string = '不明'
cmdline = [sys.executable, '-c',
'import sys; sys.stdout.buffer.write(%r)' % fail_string.encode()]
@unittest.skipIf(sys.platform != 'win32', 'Windows only')
def test_normal(self):
self.assertRaises(UnicodeDecodeError, subprocess.check_output,
self.cmdline, universal_newlines=True)
@unittest.skipIf(sys.platform != 'win32', 'Windows only')
def test_patched(self):
with unittest.mock.patch('io.TextIOWrapper', new=DummyTextIOWrapper):
self.assertEqual(
subprocess.check_output(self.cmdline, universal_newlines=True),
self.fail_string)
if __name__ == '__main__':
unittest.main()
unittest.mock.patch はとても便利です。ただ標準モジュールにぶち込んだのは頭を疑います。