解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
19.92 KiB
X
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using NewLife;
using NewLife.Buffers;
using NewLife.Caching;
using NewLife.Collections;
using NewLife.Compression;
using NewLife.Configuration;
using NewLife.Data;
using NewLife.Http;
using NewLife.Log;
using NewLife.Messaging;
using NewLife.Model;
using NewLife.Net;
using NewLife.Reflection;
using NewLife.Remoting;
using NewLife.Serialization;
using NewLife.Threading;
using NewLife.Web;

namespace Mobile;

/// <summary>移动端冒烟用例。全部用例只依赖本机回环,不访问外部网络。</summary>
public static class SmokeCases
{
    /// <summary>枚举全部用例。</summary>
    /// <returns>名称与执行委托。</returns>
    public static IEnumerable<(String Name, Action Action)> All()
    {
        yield return ("Runtime 平台信息", RuntimeInfo);
        yield return ("Utility/StringHelper", UtilityAndString);
        yield return ("MemoryCache 存取", MemoryCacheRoundTrip);
        yield return ("TimerX 定时回调", TimerXCallback);
        yield return ("JsonHelper 往返", JsonRoundTrip);
        yield return ("JsonConfigProvider 往返", ConfigRoundTrip);
        yield return ("TextFileLog 文件日志", FileLogRoundTrip);
        yield return ("PathHelper/Setting 可写路径", PathAndSetting);
        yield return ("MachineInfo 平台信息", MachineInfoRead);
        yield return ("Security 哈希与 AES", SecurityRoundTrip);
        yield return ("SpanSerializer 往返", SpanSerializerRoundTrip);
        yield return ("Binary 写入", BinaryWrite);
        yield return ("TarFile 归档往返", TarRoundTrip);
        yield return ("System.Net.Sockets TCP 回环", RawTcpLoopback);
        yield return ("NetServer/NetClient TCP 回环", TcpLoopback);
        yield return ("HttpServer/ApiHttpClient 回环", HttpLoopback);
        yield return ("WebSocket 回环", WebSocketLoopback);
        yield return ("ObjectContainer 服务解析", ObjectContainerResolve);
        yield return ("PluginHelper 动态能力守卫", PluginGuard);
        yield return ("Type.GetTypeEx 反射", TypeReflection);
        yield return ("ProcessHelper 进程信息", ProcessInfo);
    }

    private static void RuntimeInfo()
    {
        if (String.IsNullOrEmpty(RuntimeInformation.OSDescription)) throw new Exception("OSDescription 为空");
        if (String.IsNullOrEmpty(RuntimeInformation.FrameworkDescription)) throw new Exception("FrameworkDescription 为空");
        if (Runtime.TickCount64 <= 0) throw new Exception("TickCount64 非正");

        SmokeEnvironment.WriteDiagnostic($"[diag] os={RuntimeInformation.OSDescription}");
        SmokeEnvironment.WriteDiagnostic($"[diag] framework={RuntimeInformation.FrameworkDescription}");
        SmokeEnvironment.WriteDiagnostic($"[diag] dynamicCode={RuntimeFeature.IsDynamicCodeSupported}");
        SmokeEnvironment.WriteDiagnostic($"[diag] android={OperatingSystem.IsAndroid()} ios={OperatingSystem.IsIOS()} windows={OperatingSystem.IsWindows()} linux={OperatingSystem.IsLinux()}");
    }

    private static void UtilityAndString()
    {
        if ("123".ToInt() != 123) throw new Exception("ToInt 不一致");
        if (Math.Abs("3.14".ToDouble() - 3.14) > 0.0001) throw new Exception("ToDouble 不一致");
        if (!"true".ToBoolean()) throw new Exception("ToBoolean 不一致");
        if ("2026-10-08".ToDateTime().Year != 2026) throw new Exception("ToDateTime 不一致");

        var sb = Pool.StringBuilder.Get();
        try
        {
            sb.Append("mobile").Append('-').Append("smoke");
            if (sb.ToString() != "mobile-smoke") throw new Exception("Pool.StringBuilder 不一致");
        }
        finally
        {
            Pool.StringBuilder.Return(sb);
        }
    }

    private static void MemoryCacheRoundTrip()
    {
        var cache = MemoryCache.Instance;
        var key = "mobile-smoke-" + Guid.NewGuid().ToString("N");
        try
        {
            if (!cache.Set(key, "value-1", 60)) throw new Exception("MemoryCache.Set 返回 false");
            if (cache.Get<String>(key) != "value-1") throw new Exception("MemoryCache.Get 不一致");
        }
        finally
        {
            cache.Remove(key);
        }
    }

    private static void TimerXCallback()
    {
        using var signal = new ManualResetEventSlim(false);
        using var timer = new TimerX(_ =>
        {
            signal.Set();
            return Task.CompletedTask;
        }, null, 10, 0);

        if (!signal.Wait(5000)) throw new TimeoutException("TimerX 未在 5 秒内回调");
    }

    private static void JsonRoundTrip()
    {
        var model = new SmokeConfig { Name = "mobile", Level = 3 };
        var json = JsonHelper.ToJson(model);
        var back = json.ToJsonEntity<SmokeConfig>();
        if (back == null || back.Name != model.Name || back.Level != model.Level) throw new Exception($"JsonHelper 往返不一致: {json}");
    }

    private static void ConfigRoundTrip()
    {
        var dir = SmokeEnvironment.NewTempDirectory("cfg");
        var file = Path.Combine(dir, "cfg.json");
        var provider = new JsonConfigProvider { FileName = file };
        if (!provider.Save(new SmokeConfig { Name = "cfg", Level = 5 })) throw new Exception("JsonConfigProvider.Save 返回 false");

        var back = provider.Load<SmokeConfig>();
        if (back == null || back.Name != "cfg" || back.Level != 5) throw new Exception("JsonConfigProvider.Load 不一致");
    }

    private static void FileLogRoundTrip()
    {
        var dir = SmokeEnvironment.NewTempDirectory("log");
        var log = new TextFileLog { LogPath = dir };
        var saved = XTrace.Log;
        try
        {
            XTrace.Log = log;
            XTrace.WriteLine("mobile-smoke-log");
        }
        finally
        {
            XTrace.Log = saved;
            log.Dispose();
        }

        for (var i = 0; i < 30 && !HasFiles(dir); i++) Thread.Sleep(100);
        if (!HasFiles(dir)) throw new Exception("3 秒内未生成日志文件");
    }

    private static void PathAndSetting()
    {
        if (String.IsNullOrEmpty(PathHelper.BasePath)) throw new Exception("PathHelper.BasePath 为空");

        var file = Path.Combine(SmokeEnvironment.WritableRoot, "path-" + Guid.NewGuid().ToString("N")[..8] + ".txt");
        File.WriteAllText(file, "hello-mobile");
        if (File.ReadAllText(file) != "hello-mobile") throw new Exception("可写路径文件往返失败");

        SmokeEnvironment.WriteDiagnostic($"[diag] BasePath={PathHelper.BasePath}");
        SmokeEnvironment.WriteDiagnostic($"[diag] Setting.LogPath={Setting.Current.LogPath}");
        SmokeEnvironment.WriteDiagnostic($"[diag] WritableRoot={SmokeEnvironment.WritableRoot}");
    }

    private static void MachineInfoRead()
    {
        var mi = new MachineInfo();
        mi.Init();

        if (String.IsNullOrEmpty(mi.OSName)) throw new Exception("MachineInfo.OSName 为空");
        if (String.IsNullOrEmpty(mi.OSVersion)) throw new Exception("MachineInfo.OSVersion 为空");
        if (OperatingSystem.IsIOS() && mi.OSName != "iOS") throw new Exception($"MachineInfo.OSName 异常:{mi.OSName}");
        if (OperatingSystem.IsIOS() && mi.OSVersion.StartsWithIgnoreCase("Unix")) throw new Exception($"MachineInfo.OSVersion 未取到系统产品版本:{mi.OSVersion}");
        if (mi.Memory == 0)
        {
            if (OperatingSystem.IsAndroid() || OperatingSystem.IsIOS()) throw new Exception("MachineInfo.Memory 未采集");
            SmokeEnvironment.WriteDiagnostic("[diag] MachineInfo.Memory 未采集(当前平台未提供)");
        }

        SmokeEnvironment.WriteDiagnostic($"[diag] OSName={mi.OSName}; OSVersion={mi.OSVersion}; Memory={mi.Memory}; Available={mi.AvailableMemory}");
        mi.Refresh();
        if (mi.AvailableMemory > mi.Memory) throw new Exception($"AvailableMemory 异常 {mi.AvailableMemory}/{mi.Memory}");
    }

    private static void SecurityRoundTrip()
    {
        var text = "mobile-smoke";
        var md5 = text.MD5();
        if (md5.Length != 32) throw new Exception("MD5 长度异常");

        var data = Encoding.UTF8.GetBytes(text);
        var sha = data.SHA256();
        if (sha.Length != 32) throw new Exception("SHA256 长度异常");

        var key = Encoding.UTF8.GetBytes("1234567890123456");
        using var aes = Aes.Create();
        var encrypted = aes.Encrypt(data, key);
        var decrypted = aes.Decrypt(encrypted, key);
        if (Encoding.UTF8.GetString(decrypted) != text) throw new Exception("AES 往返不一致");
    }

    private static void SpanSerializerRoundTrip()
    {
        var model = new SmokeConfig { Name = "span", Level = 7 };
        var buffer = new Byte[256];
        var writer = new SpanWriter(buffer);
        SpanSerializer.WriteObject(ref writer, model, typeof(SmokeConfig));

        var reader = new SpanReader(buffer.AsSpan(0, writer.WrittenCount));
        var back = (SmokeConfig)SpanSerializer.ReadObject(ref reader, typeof(SmokeConfig))!;
        if (back.Name != model.Name || back.Level != model.Level) throw new Exception("SpanSerializer 往返不一致");
    }

    private static void BinaryWrite()
    {
        var bn = new Binary { IsLittleEndian = true };
        bn.Write(42);
        bn.Write("hello");
        var data = bn.GetPacket().ToArray();
        if (data.Length == 0) throw new Exception("Binary 写入为空");
    }

    private static void TarRoundTrip()
    {
        var root = SmokeEnvironment.NewTempDirectory("tar");
        var src = Path.Combine(root, "src");
        Directory.CreateDirectory(src);
        File.WriteAllText(Path.Combine(src, "a.txt"), "hello-tar");

        var archive = Path.Combine(root, "smoke.tar");
        TarFile.CreateFromDirectory(src, archive);

        var dst = Path.Combine(root, "dst");
        Directory.CreateDirectory(dst);
        TarFile.ExtractToDirectory(archive, dst, true);

        var file = Path.Combine(dst, "a.txt");
        if (!File.Exists(file) || File.ReadAllText(file) != "hello-tar") throw new Exception("TarFile 往返不一致");
    }

    private static void RawTcpLoopback()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        SmokeEnvironment.WriteDiagnostic($"[diag] raw tcp listener port={port}");

        var serverGot = new TaskCompletionSource<Byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
        _ = Task.Run(() =>
        {
            try
            {
                using var client = listener.AcceptTcpClient();
                using var stream = client.GetStream();
                var buffer = new Byte[4];
                var count = stream.Read(buffer, 0, buffer.Length);
                serverGot.TrySetResult(buffer[..count]);
                stream.Write("pong"u8.ToArray());
            }
            catch (Exception ex)
            {
                serverGot.TrySetException(ex);
            }
        });

        using var tcp = new TcpClient();
        tcp.Connect(IPAddress.Loopback, port);
        using var ns = tcp.GetStream();
        ns.Write("ping"u8.ToArray());

        if (!serverGot.Task.Wait(5000)) throw new TimeoutException($"raw TCP 服务端未收到请求(port={port})");
        if (serverGot.Task.IsFaulted) throw new Exception($"raw TCP 服务端异常(port={port})", serverGot.Task.Exception);

        var reply = new Byte[4];
        ns.ReadExactly(reply);
        if (Encoding.UTF8.GetString(reply) != "pong") throw new Exception($"raw TCP 应答不一致(port={port})");
    }

    private static void TcpLoopback()
    {
        using var server = new NetServer(IPAddress.Loopback, 0, NetType.Tcp) { Protocol = new SrmpCodec() };
        server.Log = XTrace.Log;
        server.SessionLog = XTrace.Log;
        server.Start();
        var state = $"active={server.Active} port={server.Port} servers={server.Servers.Count} sessions={server.SessionCount} local={server.Server?.Local}";
        SmokeEnvironment.WriteDiagnostic($"[diag] tcp server {state}");

        var serverGot = new TaskCompletionSource<Byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
        server.Received += (s, e) =>
        {
            if (e.Message is not DefaultMessage msg) return;

            var all = msg.Body!.ReadAllAsync().AsTask().GetAwaiter().GetResult();
            var body = all.AsReadOnlySequence().ToArray();
            all.TryDispose();
            serverGot.TrySetResult(body);

            if (msg.CreateReply() is not DefaultMessage reply) return;
            reply.SetBody(new ArrayPacket("pong"u8.ToArray()));
            ((INetSession)s!).SendMessage(reply);
        };

        using var client = new NetClient($"tcp://127.0.0.1:{server.Port}") { Protocol = new SrmpCodec() };
        client.Log = XTrace.Log;

        var clientGot = new TaskCompletionSource<Byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
        client.Received += (s, e) =>
        {
            if (e.Message is not DefaultMessage msg || msg.Kind < MessageKinds.Response) return;

            var all = msg.Body!.ReadAllAsync().AsTask().GetAwaiter().GetResult();
            var body = all.AsReadOnlySequence().ToArray();
            all.TryDispose();
            clientGot.TrySetResult(body);
        };

        if (!client.Open()) throw new Exception($"NetClient 打开失败({state})");

        var request = new DefaultMessage { Sequence = 0x35 };
        request.SetBody(new ArrayPacket("ping"u8.ToArray()));
        client.SendMessage(request);

        if (!serverGot.Task.Wait(5000)) throw new TimeoutException($"服务端未收到请求({state})");
        if (!clientGot.Task.Wait(5000)) throw new TimeoutException($"客户端未收到应答({state})");
        if (Encoding.UTF8.GetString(clientGot.Task.Result) != "pong") throw new Exception($"TCP 应答不一致({state})");
    }

    private static void HttpLoopback()
    {
        using var server = new HttpServer { Port = 0, AddressFamily = AddressFamily.InterNetwork };
        server.Local.Address = IPAddress.Loopback;
        server.Log = XTrace.Log;
        server.SessionLog = XTrace.Log;
        server.Map("/txt", () => "pong-mobile");
        server.Map("/obj", () => new SmokePing { Msg = "pong-mobile" });
        server.Start();
        var state = $"active={server.Active} port={server.Port} servers={server.Servers.Count}";
        SmokeEnvironment.WriteDiagnostic($"[diag] http server {state}");

        try
        {
            using var hc = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
            var text = hc.GetStringAsync($"http://127.0.0.1:{server.Port}/txt").GetAwaiter().GetResult();
            if (text != "pong-mobile") throw new Exception("HTTP 文本响应不一致: " + text);

            var api = new ApiHttpClient($"http://127.0.0.1:{server.Port}");
            var obj = api.GetAsync<SmokePing>("/obj").GetAwaiter().GetResult();
            if (obj?.Msg != "pong-mobile") throw new Exception("ApiHttpClient 对象响应不一致: " + (obj?.Msg ?? "null"));
        }
        catch (Exception ex)
        {
            throw new Exception($"HTTP 回环失败({state})", ex);
        }
    }

    private static void WebSocketLoopback()
    {
        using var server = new HttpServer { Port = 0, AddressFamily = AddressFamily.InterNetwork };
        server.Local.Address = IPAddress.Loopback;
        server.Log = XTrace.Log;
        server.SessionLog = XTrace.Log;
        server.Map("/ws", new SmokeWsHandler());
        server.Start();
        var state = $"active={server.Active} port={server.Port} servers={server.Servers.Count}";
        SmokeEnvironment.WriteDiagnostic($"[diag] ws server {state}");

        var ws = new WebSocketClient($"ws://127.0.0.1:{server.Port}/ws") { Log = XTrace.Log };
        if (!ws.OpenAsync().GetAwaiter().GetResult()) throw new Exception($"WebSocket 建连失败({state})");

        ws.SendTextAsync("hi-mobile").GetAwaiter().GetResult();
        var msg = ws.ReceiveMessageAsync().WaitAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
        var text = msg?.Payload?.ToStr() ?? "";
        if (!text.Contains("echo:hi-mobile")) throw new Exception("WebSocket 回显不一致: " + text);

        ws.CloseAsync(1000, "done").GetAwaiter().GetResult();
    }

    private static void ObjectContainerResolve()
    {
        var ioc = ObjectContainer.Current;
        ioc.AddTransient<ISmokeSvc, SmokeSvc>();

        var svc = ioc.GetService<ISmokeSvc>();
        if (svc == null || svc.GetType() != typeof(SmokeSvc)) throw new Exception("ObjectContainer 解析失败");
    }

    [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "冒烟刻意调用带 RequiresUnreferencedCode 的插件守卫 API,验证其降级行为")]
    private static void PluginGuard()
    {
        if (RuntimeFeature.IsDynamicCodeSupported)
        {
            var type = PluginHelper.LoadPlugin("NewLife.XXX.NotExist", null, null, null);
            if (type != null) throw new Exception("意外解析到类型: " + type.FullName);
            return;
        }

        try
        {
            PluginHelper.LoadPlugin("NewLife.XXX.NotExist", null, null, null);
            throw new Exception("AOT 下插件动态加载竟然可用");
        }
        catch (PlatformNotSupportedException ex)
        {
            if (!ex.Message.Contains("插件动态加载") && !ex.Message.Contains("plugin dynamic loading")) throw new Exception("异常信息不明确: " + ex.Message);
        }
    }

    private static void TypeReflection()
    {
        var type = "NewLife.Log.XTrace".GetTypeEx();
        if (type == null) throw new Exception("Type.GetTypeEx 返回 null(类型可能被裁剪)");
    }

    private static void ProcessInfo()
    {
        try
        {
            var p = Process.GetCurrentProcess();
            if (p.Id <= 0) throw new Exception("进程 Id 异常");

            var name = p.GetProcessName();
            if (String.IsNullOrEmpty(name)) throw new Exception("进程名为空");

            SmokeEnvironment.WriteDiagnostic($"[diag] process={name}; id={p.Id}; ws={p.WorkingSet64 / 1024 / 1024}MB");
        }
        catch (Exception ex) when (ex is PlatformNotSupportedException or NotSupportedException or InvalidOperationException)
        {
            throw new SkipException(ex.Message);
        }
    }

    private static Boolean HasFiles(String dir)
    {
        try
        {
            return Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories).Any();
        }
        catch
        {
            return false;
        }
    }
}

/// <summary>配置/序列化冒烟模型。</summary>
public class SmokeConfig
{
    /// <summary>名称。</summary>
    public String Name { get; set; } = "";

    /// <summary>级别。</summary>
    public Int32 Level { get; set; }
}

/// <summary>HTTP 对象响应模型。</summary>
public class SmokePing
{
    /// <summary>消息。</summary>
    public String Msg { get; set; } = "";
}

/// <summary>对象容器冒烟服务。</summary>
public interface ISmokeSvc
{
    /// <summary>返回固定响应。</summary>
    /// <returns>pong。</returns>
    String Ping();
}

/// <summary>对象容器冒烟服务实现。</summary>
public class SmokeSvc : ISmokeSvc
{
    /// <summary>返回固定响应。</summary>
    /// <returns>pong。</returns>
    public String Ping() => "pong";
}

/// <summary>WebSocket 回显处理器。</summary>
public class SmokeWsHandler : WebSocketHandler
{
    /// <summary>处理消息。</summary>
    /// <param name="socket">WebSocket 会话。</param>
    /// <param name="message">消息。</param>
    public override void ProcessMessage(NewLife.Http.WebSocket socket, WsMessage message)
    {
        if (message.Type == WebSocketMessageType.Text)
        {
            socket.Send($"echo:{message.Payload?.ToStr()}");
            return;
        }

        base.ProcessMessage(socket, message);
    }
}